Committed by
GitHub
Merge branch 'master' into dependabot/pub/lint-2.0.1
Showing
9 changed files
with
443 additions
and
119 deletions
| @@ -45,12 +45,9 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities: | @@ -45,12 +45,9 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities: | ||
| 45 | Add this to `web/index.html`: | 45 | Add this to `web/index.html`: |
| 46 | 46 | ||
| 47 | ```html | 47 | ```html |
| 48 | -<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script> | 48 | +<script type="text/javascript" src="https://unpkg.com/@zxing/library@0.19.1"></script> |
| 49 | ``` | 49 | ``` |
| 50 | 50 | ||
| 51 | -Web only supports QR codes for now. | ||
| 52 | -Do you have experience with Flutter Web development? [Help me with migrating from jsQR to qr-scanner for full barcode support!](https://github.com/juliansteenbakker/mobile_scanner/issues/54) | ||
| 53 | - | ||
| 54 | ## Features Supported | 51 | ## Features Supported |
| 55 | 52 | ||
| 56 | | Features | Android | iOS | macOS | Web | | 53 | | Features | Android | iOS | macOS | Web | |
| @@ -28,8 +28,8 @@ | @@ -28,8 +28,8 @@ | ||
| 28 | 28 | ||
| 29 | <title>example</title> | 29 | <title>example</title> |
| 30 | <link rel="manifest" href="manifest.json"> | 30 | <link rel="manifest" href="manifest.json"> |
| 31 | -<!-- <script src="https://cdn.jsdelivr.net/npm/qr-scanner@1.4.1/qr-scanner.min.js"></script>--> | ||
| 32 | <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script> | 31 | <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script> |
| 32 | + <script type="text/javascript" src="https://unpkg.com/@zxing/library@0.19.1"></script> | ||
| 33 | </head> | 33 | </head> |
| 34 | <body> | 34 | <body> |
| 35 | <!-- This script installs service_worker.js to provide PWA functionality to | 35 | <!-- This script installs service_worker.js to provide PWA functionality to |
lib/mobile_scanner_web.dart
0 → 100644
| @@ -2,12 +2,10 @@ import 'dart:async'; | @@ -2,12 +2,10 @@ import 'dart:async'; | ||
| 2 | import 'dart:html' as html; | 2 | import 'dart:html' as html; |
| 3 | import 'dart:ui' as ui; | 3 | import 'dart:ui' as ui; |
| 4 | 4 | ||
| 5 | -import 'package:flutter/material.dart'; | ||
| 6 | import 'package:flutter/services.dart'; | 5 | import 'package:flutter/services.dart'; |
| 7 | import 'package:flutter_web_plugins/flutter_web_plugins.dart'; | 6 | import 'package:flutter_web_plugins/flutter_web_plugins.dart'; |
| 7 | +import 'package:mobile_scanner/mobile_scanner_web.dart'; | ||
| 8 | import 'package:mobile_scanner/src/enums/camera_facing.dart'; | 8 | import 'package:mobile_scanner/src/enums/camera_facing.dart'; |
| 9 | -import 'package:mobile_scanner/src/web/jsqr.dart'; | ||
| 10 | -import 'package:mobile_scanner/src/web/media.dart'; | ||
| 11 | 9 | ||
| 12 | /// This plugin is the web implementation of mobile_scanner. | 10 | /// This plugin is the web implementation of mobile_scanner. |
| 13 | /// It only supports QR codes. | 11 | /// It only supports QR codes. |
| @@ -32,20 +30,14 @@ class MobileScannerWebPlugin { | @@ -32,20 +30,14 @@ class MobileScannerWebPlugin { | ||
| 32 | // Controller to send events back to the framework | 30 | // Controller to send events back to the framework |
| 33 | StreamController controller = StreamController.broadcast(); | 31 | StreamController controller = StreamController.broadcast(); |
| 34 | 32 | ||
| 35 | - // The video stream. Will be initialized later to see which camera needs to be used. | ||
| 36 | - html.MediaStream? _localStream; | ||
| 37 | - html.VideoElement video = html.VideoElement(); | ||
| 38 | - | ||
| 39 | // ID of the video feed | 33 | // ID of the video feed |
| 40 | String viewID = 'WebScanner-${DateTime.now().millisecondsSinceEpoch}'; | 34 | String viewID = 'WebScanner-${DateTime.now().millisecondsSinceEpoch}'; |
| 41 | 35 | ||
| 42 | - // Determine wether device has flas | ||
| 43 | - bool hasFlash = false; | ||
| 44 | - | ||
| 45 | - // Timer used to capture frames to be analyzed | ||
| 46 | - Timer? _frameInterval; | 36 | + static final html.DivElement vidDiv = html.DivElement(); |
| 47 | 37 | ||
| 48 | - html.DivElement vidDiv = html.DivElement(); | 38 | + static WebBarcodeReaderBase barCodeReader = |
| 39 | + ZXingBarcodeReader(videoContainer: vidDiv); | ||
| 40 | + StreamSubscription? _barCodeStreamSubscription; | ||
| 49 | 41 | ||
| 50 | /// Handle incomming messages | 42 | /// Handle incomming messages |
| 51 | Future<dynamic> handleMethodCall(MethodCall call) async { | 43 | Future<dynamic> handleMethodCall(MethodCall call) async { |
| @@ -67,20 +59,11 @@ class MobileScannerWebPlugin { | @@ -67,20 +59,11 @@ class MobileScannerWebPlugin { | ||
| 67 | 59 | ||
| 68 | /// Can enable or disable the flash if available | 60 | /// Can enable or disable the flash if available |
| 69 | Future<void> _torch(arguments) async { | 61 | Future<void> _torch(arguments) async { |
| 70 | - if (hasFlash) { | ||
| 71 | - final track = _localStream?.getVideoTracks(); | ||
| 72 | - await track!.first.applyConstraints({ | ||
| 73 | - 'advanced': {'torch': arguments == 1} | ||
| 74 | - }); | ||
| 75 | - } else { | ||
| 76 | - controller.addError('Device has no flash'); | ||
| 77 | - } | 62 | + barCodeReader.toggleTorch(enabled: arguments == 1); |
| 78 | } | 63 | } |
| 79 | 64 | ||
| 80 | /// Starts the video stream and the scanner | 65 | /// Starts the video stream and the scanner |
| 81 | Future<Map> _start(Map arguments) async { | 66 | Future<Map> _start(Map arguments) async { |
| 82 | - vidDiv.children = [video]; | ||
| 83 | - | ||
| 84 | var cameraFacing = CameraFacing.front; | 67 | var cameraFacing = CameraFacing.front; |
| 85 | if (arguments.containsKey('facing')) { | 68 | if (arguments.containsKey('facing')) { |
| 86 | cameraFacing = CameraFacing.values[arguments['facing'] as int]; | 69 | cameraFacing = CameraFacing.values[arguments['facing'] as int]; |
| @@ -90,64 +73,45 @@ class MobileScannerWebPlugin { | @@ -90,64 +73,45 @@ class MobileScannerWebPlugin { | ||
| 90 | // ignore: UNDEFINED_PREFIXED_NAME, avoid_dynamic_calls | 73 | // ignore: UNDEFINED_PREFIXED_NAME, avoid_dynamic_calls |
| 91 | ui.platformViewRegistry.registerViewFactory( | 74 | ui.platformViewRegistry.registerViewFactory( |
| 92 | viewID, | 75 | viewID, |
| 93 | - (int id) => vidDiv | 76 | + (int id) { |
| 77 | + return vidDiv | ||
| 94 | ..style.width = '100%' | 78 | ..style.width = '100%' |
| 95 | - ..style.height = '100%', | 79 | + ..style.height = '100%'; |
| 80 | + }, | ||
| 96 | ); | 81 | ); |
| 97 | 82 | ||
| 98 | // Check if stream is running | 83 | // Check if stream is running |
| 99 | - if (_localStream != null) { | 84 | + if (barCodeReader.isStarted) { |
| 100 | return { | 85 | return { |
| 101 | 'ViewID': viewID, | 86 | 'ViewID': viewID, |
| 102 | - 'videoWidth': video.videoWidth, | ||
| 103 | - 'videoHeight': video.videoHeight | 87 | + 'videoWidth': barCodeReader.videoWidth, |
| 88 | + 'videoHeight': barCodeReader.videoHeight, | ||
| 89 | + 'torchable': barCodeReader.hasTorch, | ||
| 104 | }; | 90 | }; |
| 105 | } | 91 | } |
| 106 | - | ||
| 107 | try { | 92 | try { |
| 108 | - // Check if browser supports multiple camera's and set if supported | ||
| 109 | - final Map? capabilities = | ||
| 110 | - html.window.navigator.mediaDevices?.getSupportedConstraints(); | ||
| 111 | - if (capabilities != null && capabilities['facingMode'] as bool) { | ||
| 112 | - final constraints = { | ||
| 113 | - 'video': VideoOptions( | ||
| 114 | - facingMode: | ||
| 115 | - cameraFacing == CameraFacing.front ? 'user' : 'environment', | ||
| 116 | - ) | ||
| 117 | - }; | 93 | + await barCodeReader.start( |
| 94 | + cameraFacing: cameraFacing, | ||
| 95 | + ); | ||
| 118 | 96 | ||
| 119 | - _localStream = | ||
| 120 | - await html.window.navigator.mediaDevices?.getUserMedia(constraints); | ||
| 121 | - } else { | ||
| 122 | - _localStream = await html.window.navigator.mediaDevices | ||
| 123 | - ?.getUserMedia({'video': true}); | 97 | + _barCodeStreamSubscription = |
| 98 | + barCodeReader.detectBarcodeContinuously().listen((code) { | ||
| 99 | + if (code != null) { | ||
| 100 | + controller.add({ | ||
| 101 | + 'name': 'barcodeWeb', | ||
| 102 | + 'data': { | ||
| 103 | + 'rawValue': code.rawValue, | ||
| 104 | + 'rawBytes': code.rawBytes, | ||
| 105 | + }, | ||
| 106 | + }); | ||
| 124 | } | 107 | } |
| 125 | - | ||
| 126 | - video.srcObject = _localStream; | ||
| 127 | - | ||
| 128 | - // TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533 | ||
| 129 | - // final track = _localStream?.getVideoTracks(); | ||
| 130 | - // if (track != null) { | ||
| 131 | - // final imageCapture = html.ImageCapture(track.first); | ||
| 132 | - // final photoCapabilities = await imageCapture.getPhotoCapabilities(); | ||
| 133 | - // } | ||
| 134 | - | ||
| 135 | - // required to tell iOS safari we don't want fullscreen | ||
| 136 | - video.setAttribute('playsinline', 'true'); | ||
| 137 | - | ||
| 138 | - await video.play(); | ||
| 139 | - | ||
| 140 | - // Then capture a frame to be analyzed every 200 miliseconds | ||
| 141 | - _frameInterval = | ||
| 142 | - Timer.periodic(const Duration(milliseconds: 200), (timer) { | ||
| 143 | - _captureFrame(); | ||
| 144 | }); | 108 | }); |
| 145 | 109 | ||
| 146 | return { | 110 | return { |
| 147 | 'ViewID': viewID, | 111 | 'ViewID': viewID, |
| 148 | - 'videoWidth': video.videoWidth, | ||
| 149 | - 'videoHeight': video.videoHeight, | ||
| 150 | - 'torchable': hasFlash | 112 | + 'videoWidth': barCodeReader.videoWidth, |
| 113 | + 'videoHeight': barCodeReader.videoHeight, | ||
| 114 | + 'torchable': barCodeReader.hasTorch, | ||
| 151 | }; | 115 | }; |
| 152 | } catch (e) { | 116 | } catch (e) { |
| 153 | throw PlatformException(code: 'MobileScannerWeb', message: '$e'); | 117 | throw PlatformException(code: 'MobileScannerWeb', message: '$e'); |
| @@ -170,40 +134,8 @@ class MobileScannerWebPlugin { | @@ -170,40 +134,8 @@ class MobileScannerWebPlugin { | ||
| 170 | 134 | ||
| 171 | /// Stops the video feed and analyzer | 135 | /// Stops the video feed and analyzer |
| 172 | Future<void> cancel() async { | 136 | Future<void> cancel() async { |
| 173 | - try { | ||
| 174 | - // Stop the camera stream | ||
| 175 | - _localStream?.getTracks().forEach((track) { | ||
| 176 | - if (track.readyState == 'live') { | ||
| 177 | - track.stop(); | ||
| 178 | - } | ||
| 179 | - }); | ||
| 180 | - } catch (e) { | ||
| 181 | - debugPrint('Failed to stop stream: $e'); | ||
| 182 | - } | ||
| 183 | - | ||
| 184 | - video.srcObject = null; | ||
| 185 | - _localStream = null; | ||
| 186 | - _frameInterval?.cancel(); | ||
| 187 | - _frameInterval = null; | ||
| 188 | - } | ||
| 189 | - | ||
| 190 | - /// Captures a frame and analyzes it for QR codes | ||
| 191 | - Future<dynamic> _captureFrame() async { | ||
| 192 | - if (_localStream == null) return null; | ||
| 193 | - final canvas = | ||
| 194 | - html.CanvasElement(width: video.videoWidth, height: video.videoHeight); | ||
| 195 | - final ctx = canvas.context2D; | ||
| 196 | - | ||
| 197 | - ctx.drawImage(video, 0, 0); | ||
| 198 | - final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!); | ||
| 199 | - | ||
| 200 | - final code = jsQR(imgData.data, canvas.width, canvas.height); | ||
| 201 | - if (code != null) { | ||
| 202 | - controller.add({ | ||
| 203 | - 'name': 'barcodeWeb', | ||
| 204 | - 'data': code.data, | ||
| 205 | - 'binaryData': code.binaryData, | ||
| 206 | - }); | ||
| 207 | - } | 137 | + barCodeReader.stop(); |
| 138 | + await _barCodeStreamSubscription?.cancel(); | ||
| 139 | + _barCodeStreamSubscription = null; | ||
| 208 | } | 140 | } |
| 209 | } | 141 | } |
| @@ -329,11 +329,13 @@ class MobileScannerController { | @@ -329,11 +329,13 @@ class MobileScannerController { | ||
| 329 | ); | 329 | ); |
| 330 | break; | 330 | break; |
| 331 | case 'barcodeWeb': | 331 | case 'barcodeWeb': |
| 332 | + final barcode = data as Map?; | ||
| 332 | _barcodesController.add( | 333 | _barcodesController.add( |
| 333 | BarcodeCapture( | 334 | BarcodeCapture( |
| 334 | barcodes: [ | 335 | barcodes: [ |
| 335 | Barcode( | 336 | Barcode( |
| 336 | - rawValue: data as String?, | 337 | + rawValue: barcode?['rawValue'] as String?, |
| 338 | + rawBytes: barcode?['rawBytes'] as Uint8List?, | ||
| 337 | ) | 339 | ) |
| 338 | ], | 340 | ], |
| 339 | ), | 341 | ), |
lib/src/web/base.dart
0 → 100644
| 1 | +import 'dart:html'; | ||
| 2 | + | ||
| 3 | +import 'package:flutter/material.dart'; | ||
| 4 | +import 'package:mobile_scanner/src/enums/camera_facing.dart'; | ||
| 5 | +import 'package:mobile_scanner/src/objects/barcode.dart'; | ||
| 6 | +import 'package:mobile_scanner/src/web/media.dart'; | ||
| 7 | + | ||
| 8 | +abstract class WebBarcodeReaderBase { | ||
| 9 | + /// Timer used to capture frames to be analyzed | ||
| 10 | + final Duration frameInterval; | ||
| 11 | + final DivElement videoContainer; | ||
| 12 | + | ||
| 13 | + const WebBarcodeReaderBase({ | ||
| 14 | + required this.videoContainer, | ||
| 15 | + this.frameInterval = const Duration(milliseconds: 200), | ||
| 16 | + }); | ||
| 17 | + | ||
| 18 | + bool get isStarted; | ||
| 19 | + | ||
| 20 | + int get videoWidth; | ||
| 21 | + int get videoHeight; | ||
| 22 | + | ||
| 23 | + /// Starts streaming video | ||
| 24 | + Future<void> start({ | ||
| 25 | + required CameraFacing cameraFacing, | ||
| 26 | + }); | ||
| 27 | + | ||
| 28 | + /// Starts scanning QR codes or barcodes | ||
| 29 | + Stream<Barcode?> detectBarcodeContinuously(); | ||
| 30 | + | ||
| 31 | + /// Stops streaming video | ||
| 32 | + Future<void> stop(); | ||
| 33 | + | ||
| 34 | + /// Can enable or disable the flash if available | ||
| 35 | + Future<void> toggleTorch({required bool enabled}); | ||
| 36 | + | ||
| 37 | + /// Determine whether device has flash | ||
| 38 | + bool get hasTorch; | ||
| 39 | +} | ||
| 40 | + | ||
| 41 | +mixin InternalStreamCreation on WebBarcodeReaderBase { | ||
| 42 | + /// The video stream. | ||
| 43 | + /// Will be initialized later to see which camera needs to be used. | ||
| 44 | + MediaStream? localMediaStream; | ||
| 45 | + final VideoElement video = VideoElement(); | ||
| 46 | + | ||
| 47 | + @override | ||
| 48 | + int get videoWidth => video.videoWidth; | ||
| 49 | + @override | ||
| 50 | + int get videoHeight => video.videoHeight; | ||
| 51 | + | ||
| 52 | + Future<MediaStream?> initMediaStream(CameraFacing cameraFacing) async { | ||
| 53 | + // Check if browser supports multiple camera's and set if supported | ||
| 54 | + final Map? capabilities = | ||
| 55 | + window.navigator.mediaDevices?.getSupportedConstraints(); | ||
| 56 | + final Map<String, dynamic> constraints; | ||
| 57 | + if (capabilities != null && capabilities['facingMode'] as bool) { | ||
| 58 | + constraints = { | ||
| 59 | + 'video': VideoOptions( | ||
| 60 | + facingMode: | ||
| 61 | + cameraFacing == CameraFacing.front ? 'user' : 'environment', | ||
| 62 | + ) | ||
| 63 | + }; | ||
| 64 | + } else { | ||
| 65 | + constraints = {'video': true}; | ||
| 66 | + } | ||
| 67 | + final stream = | ||
| 68 | + await window.navigator.mediaDevices?.getUserMedia(constraints); | ||
| 69 | + return stream; | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + void prepareVideoElement(VideoElement videoSource); | ||
| 73 | + | ||
| 74 | + Future<void> attachStreamToVideo( | ||
| 75 | + MediaStream stream, | ||
| 76 | + VideoElement videoSource, | ||
| 77 | + ); | ||
| 78 | + | ||
| 79 | + @override | ||
| 80 | + Future<void> stop() async { | ||
| 81 | + try { | ||
| 82 | + // Stop the camera stream | ||
| 83 | + localMediaStream?.getTracks().forEach((track) { | ||
| 84 | + if (track.readyState == 'live') { | ||
| 85 | + track.stop(); | ||
| 86 | + } | ||
| 87 | + }); | ||
| 88 | + } catch (e) { | ||
| 89 | + debugPrint('Failed to stop stream: $e'); | ||
| 90 | + } | ||
| 91 | + video.srcObject = null; | ||
| 92 | + localMediaStream = null; | ||
| 93 | + videoContainer.children = []; | ||
| 94 | + } | ||
| 95 | +} | ||
| 96 | + | ||
| 97 | +/// Mixin for libraries that don't have built-in torch support | ||
| 98 | +mixin InternalTorchDetection on InternalStreamCreation { | ||
| 99 | + @override | ||
| 100 | + bool get hasTorch { | ||
| 101 | + // TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533 | ||
| 102 | + // final track = _localStream?.getVideoTracks(); | ||
| 103 | + // if (track != null) { | ||
| 104 | + // final imageCapture = html.ImageCapture(track.first); | ||
| 105 | + // final photoCapabilities = await imageCapture.getPhotoCapabilities(); | ||
| 106 | + // } | ||
| 107 | + return false; | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + @override | ||
| 111 | + Future<void> toggleTorch({required bool enabled}) async { | ||
| 112 | + if (hasTorch) { | ||
| 113 | + final track = localMediaStream?.getVideoTracks(); | ||
| 114 | + await track?.first.applyConstraints({ | ||
| 115 | + 'advanced': [ | ||
| 116 | + {'torch': enabled} | ||
| 117 | + ] | ||
| 118 | + }); | ||
| 119 | + } | ||
| 120 | + } | ||
| 121 | +} |
| 1 | @JS() | 1 | @JS() |
| 2 | library jsqr; | 2 | library jsqr; |
| 3 | 3 | ||
| 4 | +import 'dart:async'; | ||
| 5 | +import 'dart:html'; | ||
| 4 | import 'dart:typed_data'; | 6 | import 'dart:typed_data'; |
| 5 | 7 | ||
| 6 | import 'package:js/js.dart'; | 8 | import 'package:js/js.dart'; |
| 9 | +import 'package:mobile_scanner/src/enums/camera_facing.dart'; | ||
| 10 | +import 'package:mobile_scanner/src/objects/barcode.dart'; | ||
| 11 | +import 'package:mobile_scanner/src/web/base.dart'; | ||
| 7 | 12 | ||
| 8 | @JS('jsQR') | 13 | @JS('jsQR') |
| 9 | external Code? jsQR(dynamic data, int? width, int? height); | 14 | external Code? jsQR(dynamic data, int? width, int? height); |
| @@ -14,3 +19,72 @@ class Code { | @@ -14,3 +19,72 @@ class Code { | ||
| 14 | 19 | ||
| 15 | external Uint8ClampedList get binaryData; | 20 | external Uint8ClampedList get binaryData; |
| 16 | } | 21 | } |
| 22 | + | ||
| 23 | +class JsQrCodeReader extends WebBarcodeReaderBase | ||
| 24 | + with InternalStreamCreation, InternalTorchDetection { | ||
| 25 | + JsQrCodeReader({required super.videoContainer}); | ||
| 26 | + | ||
| 27 | + @override | ||
| 28 | + bool get isStarted => localMediaStream != null; | ||
| 29 | + | ||
| 30 | + @override | ||
| 31 | + Future<void> start({ | ||
| 32 | + required CameraFacing cameraFacing, | ||
| 33 | + }) async { | ||
| 34 | + videoContainer.children = [video]; | ||
| 35 | + | ||
| 36 | + final stream = await initMediaStream(cameraFacing); | ||
| 37 | + | ||
| 38 | + prepareVideoElement(video); | ||
| 39 | + if (stream != null) { | ||
| 40 | + await attachStreamToVideo(stream, video); | ||
| 41 | + } | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + @override | ||
| 45 | + void prepareVideoElement(VideoElement videoSource) { | ||
| 46 | + // required to tell iOS safari we don't want fullscreen | ||
| 47 | + videoSource.setAttribute('playsinline', 'true'); | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + @override | ||
| 51 | + Future<void> attachStreamToVideo( | ||
| 52 | + MediaStream stream, | ||
| 53 | + VideoElement videoSource, | ||
| 54 | + ) async { | ||
| 55 | + localMediaStream = stream; | ||
| 56 | + videoSource.srcObject = stream; | ||
| 57 | + await videoSource.play(); | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + @override | ||
| 61 | + Stream<Barcode?> detectBarcodeContinuously() async* { | ||
| 62 | + yield* Stream.periodic(frameInterval, (_) { | ||
| 63 | + return _captureFrame(video); | ||
| 64 | + }).asyncMap((event) async { | ||
| 65 | + final code = await event; | ||
| 66 | + if (code == null) { | ||
| 67 | + return null; | ||
| 68 | + } | ||
| 69 | + return Barcode( | ||
| 70 | + rawValue: code.data, | ||
| 71 | + rawBytes: Uint8List.fromList(code.binaryData), | ||
| 72 | + format: BarcodeFormat.qrCode, | ||
| 73 | + ); | ||
| 74 | + }); | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + /// Captures a frame and analyzes it for QR codes | ||
| 78 | + Future<Code?> _captureFrame(VideoElement video) async { | ||
| 79 | + if (localMediaStream == null) return null; | ||
| 80 | + final canvas = | ||
| 81 | + CanvasElement(width: video.videoWidth, height: video.videoHeight); | ||
| 82 | + final ctx = canvas.context2D; | ||
| 83 | + | ||
| 84 | + ctx.drawImage(video, 0, 0); | ||
| 85 | + final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!); | ||
| 86 | + | ||
| 87 | + final code = jsQR(imgData.data, canvas.width, canvas.height); | ||
| 88 | + return code; | ||
| 89 | + } | ||
| 90 | +} |
lib/src/web/qr_scanner.dart
deleted
100644 → 0
lib/src/web/zxing.dart
0 → 100644
| 1 | +import 'dart:async'; | ||
| 2 | +import 'dart:html'; | ||
| 3 | +import 'dart:typed_data'; | ||
| 4 | + | ||
| 5 | +import 'package:js/js.dart'; | ||
| 6 | +import 'package:mobile_scanner/src/enums/camera_facing.dart'; | ||
| 7 | +import 'package:mobile_scanner/src/objects/barcode.dart'; | ||
| 8 | +import 'package:mobile_scanner/src/web/base.dart'; | ||
| 9 | + | ||
| 10 | +@JS('Promise') | ||
| 11 | +@staticInterop | ||
| 12 | +class Promise<T> {} | ||
| 13 | + | ||
| 14 | +@JS('ZXing.BrowserMultiFormatReader') | ||
| 15 | +@staticInterop | ||
| 16 | +class JsZXingBrowserMultiFormatReader { | ||
| 17 | + /// https://github.com/zxing-js/library/blob/1e9ccb3b6b28d75b9eef866dba196d8937eb4449/src/browser/BrowserMultiFormatReader.ts#L11 | ||
| 18 | + external factory JsZXingBrowserMultiFormatReader( | ||
| 19 | + dynamic hints, | ||
| 20 | + int timeBetweenScansMillis, | ||
| 21 | + ); | ||
| 22 | +} | ||
| 23 | + | ||
| 24 | +@JS() | ||
| 25 | +@anonymous | ||
| 26 | +abstract class Result { | ||
| 27 | + /// raw text encoded by the barcode | ||
| 28 | + external String get text; | ||
| 29 | + | ||
| 30 | + /// Returns raw bytes encoded by the barcode, if applicable, otherwise null | ||
| 31 | + external Uint8ClampedList? get rawBytes; | ||
| 32 | + | ||
| 33 | + /// Representing the format of the barcode that was decoded | ||
| 34 | + external int? format; | ||
| 35 | +} | ||
| 36 | + | ||
| 37 | +extension ResultExt on Result { | ||
| 38 | + Barcode toBarcode() { | ||
| 39 | + final rawBytes = this.rawBytes; | ||
| 40 | + return Barcode( | ||
| 41 | + rawValue: text, | ||
| 42 | + rawBytes: rawBytes != null ? Uint8List.fromList(rawBytes) : null, | ||
| 43 | + format: barcodeFormat, | ||
| 44 | + ); | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + /// https://github.com/zxing-js/library/blob/1e9ccb3b6b28d75b9eef866dba196d8937eb4449/src/core/BarcodeFormat.ts#L28 | ||
| 48 | + BarcodeFormat get barcodeFormat { | ||
| 49 | + switch (format) { | ||
| 50 | + case 1: | ||
| 51 | + return BarcodeFormat.aztec; | ||
| 52 | + case 2: | ||
| 53 | + return BarcodeFormat.codebar; | ||
| 54 | + case 3: | ||
| 55 | + return BarcodeFormat.code39; | ||
| 56 | + case 4: | ||
| 57 | + return BarcodeFormat.code128; | ||
| 58 | + case 5: | ||
| 59 | + return BarcodeFormat.dataMatrix; | ||
| 60 | + case 6: | ||
| 61 | + return BarcodeFormat.ean8; | ||
| 62 | + case 7: | ||
| 63 | + return BarcodeFormat.ean13; | ||
| 64 | + case 8: | ||
| 65 | + return BarcodeFormat.itf; | ||
| 66 | + // case 9: | ||
| 67 | + // return BarcodeFormat.maxicode; | ||
| 68 | + case 10: | ||
| 69 | + return BarcodeFormat.pdf417; | ||
| 70 | + case 11: | ||
| 71 | + return BarcodeFormat.qrCode; | ||
| 72 | + // case 12: | ||
| 73 | + // return BarcodeFormat.rss14; | ||
| 74 | + // case 13: | ||
| 75 | + // return BarcodeFormat.rssExp; | ||
| 76 | + case 14: | ||
| 77 | + return BarcodeFormat.upcA; | ||
| 78 | + case 15: | ||
| 79 | + return BarcodeFormat.upcE; | ||
| 80 | + default: | ||
| 81 | + return BarcodeFormat.unknown; | ||
| 82 | + } | ||
| 83 | + } | ||
| 84 | +} | ||
| 85 | + | ||
| 86 | +typedef BarcodeDetectionCallback = void Function( | ||
| 87 | + Result? result, | ||
| 88 | + dynamic error, | ||
| 89 | +); | ||
| 90 | + | ||
| 91 | +extension JsZXingBrowserMultiFormatReaderExt | ||
| 92 | + on JsZXingBrowserMultiFormatReader { | ||
| 93 | + external Promise<void> decodeFromVideoElementContinuously( | ||
| 94 | + VideoElement source, | ||
| 95 | + BarcodeDetectionCallback callbackFn, | ||
| 96 | + ); | ||
| 97 | + | ||
| 98 | + /// Continuously decodes from video input | ||
| 99 | + external void decodeContinuously( | ||
| 100 | + VideoElement element, | ||
| 101 | + BarcodeDetectionCallback callbackFn, | ||
| 102 | + ); | ||
| 103 | + | ||
| 104 | + external Promise<void> decodeFromStream( | ||
| 105 | + MediaStream stream, | ||
| 106 | + VideoElement videoSource, | ||
| 107 | + BarcodeDetectionCallback callbackFn, | ||
| 108 | + ); | ||
| 109 | + | ||
| 110 | + external Promise<void> decodeFromConstraints( | ||
| 111 | + dynamic constraints, | ||
| 112 | + VideoElement videoSource, | ||
| 113 | + BarcodeDetectionCallback callbackFn, | ||
| 114 | + ); | ||
| 115 | + | ||
| 116 | + external void stopContinuousDecode(); | ||
| 117 | + | ||
| 118 | + external VideoElement prepareVideoElement(VideoElement videoSource); | ||
| 119 | + | ||
| 120 | + /// Defines what the [videoElement] src will be. | ||
| 121 | + external void addVideoSource( | ||
| 122 | + VideoElement videoElement, | ||
| 123 | + MediaStream stream, | ||
| 124 | + ); | ||
| 125 | + | ||
| 126 | + external bool isVideoPlaying(VideoElement video); | ||
| 127 | + | ||
| 128 | + external void reset(); | ||
| 129 | + | ||
| 130 | + /// The HTML video element, used to display the camera stream. | ||
| 131 | + external VideoElement? videoElement; | ||
| 132 | + | ||
| 133 | + /// The stream output from camera. | ||
| 134 | + external MediaStream? stream; | ||
| 135 | +} | ||
| 136 | + | ||
| 137 | +class ZXingBarcodeReader extends WebBarcodeReaderBase | ||
| 138 | + with InternalStreamCreation, InternalTorchDetection { | ||
| 139 | + late final JsZXingBrowserMultiFormatReader _reader = | ||
| 140 | + JsZXingBrowserMultiFormatReader( | ||
| 141 | + null, | ||
| 142 | + frameInterval.inMilliseconds, | ||
| 143 | + ); | ||
| 144 | + | ||
| 145 | + ZXingBarcodeReader({required super.videoContainer}); | ||
| 146 | + | ||
| 147 | + @override | ||
| 148 | + bool get isStarted => localMediaStream != null; | ||
| 149 | + | ||
| 150 | + @override | ||
| 151 | + Future<void> start({ | ||
| 152 | + required CameraFacing cameraFacing, | ||
| 153 | + }) async { | ||
| 154 | + videoContainer.children = [video]; | ||
| 155 | + | ||
| 156 | + final stream = await initMediaStream(cameraFacing); | ||
| 157 | + | ||
| 158 | + prepareVideoElement(video); | ||
| 159 | + if (stream != null) { | ||
| 160 | + await attachStreamToVideo(stream, video); | ||
| 161 | + } | ||
| 162 | + } | ||
| 163 | + | ||
| 164 | + @override | ||
| 165 | + void prepareVideoElement(VideoElement videoSource) { | ||
| 166 | + _reader.prepareVideoElement(videoSource); | ||
| 167 | + } | ||
| 168 | + | ||
| 169 | + @override | ||
| 170 | + Future<void> attachStreamToVideo( | ||
| 171 | + MediaStream stream, | ||
| 172 | + VideoElement videoSource, | ||
| 173 | + ) async { | ||
| 174 | + _reader.addVideoSource(videoSource, stream); | ||
| 175 | + _reader.videoElement = videoSource; | ||
| 176 | + _reader.stream = stream; | ||
| 177 | + localMediaStream = stream; | ||
| 178 | + await videoSource.play(); | ||
| 179 | + } | ||
| 180 | + | ||
| 181 | + @override | ||
| 182 | + Stream<Barcode?> detectBarcodeContinuously() { | ||
| 183 | + final controller = StreamController<Barcode?>(); | ||
| 184 | + controller.onListen = () async { | ||
| 185 | + _reader.decodeContinuously( | ||
| 186 | + video, | ||
| 187 | + allowInterop((result, error) { | ||
| 188 | + if (result != null) { | ||
| 189 | + controller.add(result.toBarcode()); | ||
| 190 | + } | ||
| 191 | + }), | ||
| 192 | + ); | ||
| 193 | + }; | ||
| 194 | + controller.onCancel = () { | ||
| 195 | + _reader.stopContinuousDecode(); | ||
| 196 | + }; | ||
| 197 | + return controller.stream; | ||
| 198 | + } | ||
| 199 | + | ||
| 200 | + @override | ||
| 201 | + Future<void> stop() async { | ||
| 202 | + _reader.reset(); | ||
| 203 | + super.stop(); | ||
| 204 | + } | ||
| 205 | +} |
-
Please register or login to post a comment