Julian Steenbakker
Committed by GitHub

Merge branch 'master' into feature/zoom

@@ -7,7 +7,7 @@ @@ -7,7 +7,7 @@
7 release-please: 7 release-please:
8 runs-on: ubuntu-latest 8 runs-on: ubuntu-latest
9 steps: 9 steps:
10 - - uses: GoogleCloudPlatform/release-please-action@v3.6.0 10 + - uses: GoogleCloudPlatform/release-please-action@v3.6.1
11 with: 11 with:
12 token: ${{ secrets.GITHUB_TOKEN }} 12 token: ${{ secrets.GITHUB_TOKEN }}
13 release-type: simple 13 release-type: simple
@@ -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 |
@@ -2,7 +2,7 @@ group 'dev.steenbakker.mobile_scanner' @@ -2,7 +2,7 @@ group 'dev.steenbakker.mobile_scanner'
2 version '1.0-SNAPSHOT' 2 version '1.0-SNAPSHOT'
3 3
4 buildscript { 4 buildscript {
5 - ext.kotlin_version = '1.7.21' 5 + ext.kotlin_version = '1.7.22'
6 repositories { 6 repositories {
7 google() 7 google()
8 mavenCentral() 8 mavenCentral()
1 buildscript { 1 buildscript {
2 - ext.kotlin_version = '1.7.21' 2 + ext.kotlin_version = '1.7.22'
3 repositories { 3 repositories {
4 google() 4 google()
5 mavenCentral() 5 mavenCentral()
@@ -77,7 +77,13 @@ class _BarcodeScannerWithControllerState @@ -77,7 +77,13 @@ class _BarcodeScannerWithControllerState
77 child: Row( 77 child: Row(
78 mainAxisAlignment: MainAxisAlignment.spaceEvenly, 78 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
79 children: [ 79 children: [
80 - IconButton( 80 + ValueListenableBuilder(
  81 + valueListenable: controller.hasTorchState,
  82 + builder: (context, state, child) {
  83 + if (state != true) {
  84 + return const SizedBox.shrink();
  85 + }
  86 + return IconButton(
81 color: Colors.white, 87 color: Colors.white,
82 icon: ValueListenableBuilder( 88 icon: ValueListenableBuilder(
83 valueListenable: controller.torchState, 89 valueListenable: controller.torchState,
@@ -104,6 +110,8 @@ class _BarcodeScannerWithControllerState @@ -104,6 +110,8 @@ class _BarcodeScannerWithControllerState
104 ), 110 ),
105 iconSize: 32.0, 111 iconSize: 32.0,
106 onPressed: () => controller.toggleTorch(), 112 onPressed: () => controller.toggleTorch(),
  113 + );
  114 + },
107 ), 115 ),
108 IconButton( 116 IconButton(
109 color: Colors.white, 117 color: Colors.white,
@@ -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
  1 +library mobile_scanner_web;
  2 +
  3 +export 'src/web/base.dart';
  4 +export 'src/web/jsqr.dart';
  5 +export 'src/web/zxing.dart';
@@ -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,51 @@ class MobileScannerWebPlugin { @@ -90,64 +73,51 @@ 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) {
  85 + final hasTorch = await barCodeReader.hasTorch();
100 return { 86 return {
101 'ViewID': viewID, 87 'ViewID': viewID,
102 - 'videoWidth': video.videoWidth,  
103 - 'videoHeight': video.videoHeight 88 + 'videoWidth': barCodeReader.videoWidth,
  89 + 'videoHeight': barCodeReader.videoHeight,
  90 + 'torchable': hasTorch,
104 }; 91 };
105 } 92 }
106 -  
107 try { 93 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 - }; 94 + await barCodeReader.start(
  95 + cameraFacing: cameraFacing,
  96 + );
118 97
119 - _localStream =  
120 - await html.window.navigator.mediaDevices?.getUserMedia(constraints);  
121 - } else {  
122 - _localStream = await html.window.navigator.mediaDevices  
123 - ?.getUserMedia({'video': true}); 98 + _barCodeStreamSubscription =
  99 + barCodeReader.detectBarcodeContinuously().listen((code) {
  100 + if (code != null) {
  101 + controller.add({
  102 + 'name': 'barcodeWeb',
  103 + 'data': {
  104 + 'rawValue': code.rawValue,
  105 + 'rawBytes': code.rawBytes,
  106 + },
  107 + });
124 } 108 }
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 }); 109 });
  110 + final hasTorch = await barCodeReader.hasTorch();
  111 +
  112 + if (hasTorch && arguments.containsKey('torch')) {
  113 + barCodeReader.toggleTorch(enabled: arguments['torch'] as bool);
  114 + }
145 115
146 return { 116 return {
147 'ViewID': viewID, 117 'ViewID': viewID,
148 - 'videoWidth': video.videoWidth,  
149 - 'videoHeight': video.videoHeight,  
150 - 'torchable': hasFlash 118 + 'videoWidth': barCodeReader.videoWidth,
  119 + 'videoHeight': barCodeReader.videoHeight,
  120 + 'torchable': hasTorch,
151 }; 121 };
152 } catch (e) { 122 } catch (e) {
153 throw PlatformException(code: 'MobileScannerWeb', message: '$e'); 123 throw PlatformException(code: 'MobileScannerWeb', message: '$e');
@@ -170,40 +140,8 @@ class MobileScannerWebPlugin { @@ -170,40 +140,8 @@ class MobileScannerWebPlugin {
170 140
171 /// Stops the video feed and analyzer 141 /// Stops the video feed and analyzer
172 Future<void> cancel() async { 142 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 - } 143 + barCodeReader.stop();
  144 + await _barCodeStreamSubscription?.cancel();
  145 + _barCodeStreamSubscription = null;
208 } 146 }
209 } 147 }
@@ -99,19 +99,21 @@ class MobileScannerController { @@ -99,19 +99,21 @@ class MobileScannerController {
99 99
100 bool isStarting = false; 100 bool isStarting = false;
101 101
102 - bool? _hasTorch; 102 + /// A notifier that provides availability of the Torch (Flash)
  103 + final ValueNotifier<bool?> hasTorchState = ValueNotifier(false);
103 104
104 /// Returns whether the device has a torch. 105 /// Returns whether the device has a torch.
105 /// 106 ///
106 /// Throws an error if the controller is not initialized. 107 /// Throws an error if the controller is not initialized.
107 bool get hasTorch { 108 bool get hasTorch {
108 - if (_hasTorch == null) { 109 + final hasTorch = hasTorchState.value;
  110 + if (hasTorch == null) {
109 throw const MobileScannerException( 111 throw const MobileScannerException(
110 errorCode: MobileScannerErrorCode.controllerUninitialized, 112 errorCode: MobileScannerErrorCode.controllerUninitialized,
111 ); 113 );
112 } 114 }
113 115
114 - return _hasTorch!; 116 + return hasTorch;
115 } 117 }
116 118
117 /// Set the starting arguments for the camera 119 /// Set the starting arguments for the camera
@@ -210,8 +212,9 @@ class MobileScannerController { @@ -210,8 +212,9 @@ class MobileScannerController {
210 ); 212 );
211 } 213 }
212 214
213 - _hasTorch = startResult['torchable'] as bool? ?? false;  
214 - if (_hasTorch! && torchEnabled) { 215 + final hasTorch = startResult['torchable'] as bool? ?? false;
  216 + hasTorchState.value = hasTorch;
  217 + if (hasTorch && torchEnabled) {
215 torchState.value = TorchState.on; 218 torchState.value = TorchState.on;
216 } 219 }
217 220
@@ -223,7 +226,7 @@ class MobileScannerController { @@ -223,7 +226,7 @@ class MobileScannerController {
223 startResult['videoHeight'] as double? ?? 0, 226 startResult['videoHeight'] as double? ?? 0,
224 ) 227 )
225 : toSize(startResult['size'] as Map? ?? {}), 228 : toSize(startResult['size'] as Map? ?? {}),
226 - hasTorch: _hasTorch!, 229 + hasTorch: hasTorch,
227 textureId: kIsWeb ? null : startResult['textureId'] as int?, 230 textureId: kIsWeb ? null : startResult['textureId'] as int?,
228 webId: kIsWeb ? startResult['ViewID'] as String? : null, 231 webId: kIsWeb ? startResult['ViewID'] as String? : null,
229 ); 232 );
@@ -244,7 +247,7 @@ class MobileScannerController { @@ -244,7 +247,7 @@ class MobileScannerController {
244 /// 247 ///
245 /// Throws if the controller was not initialized. 248 /// Throws if the controller was not initialized.
246 Future<void> toggleTorch() async { 249 Future<void> toggleTorch() async {
247 - final hasTorch = _hasTorch; 250 + final hasTorch = hasTorchState.value;
248 251
249 if (hasTorch == null) { 252 if (hasTorch == null) {
250 throw const MobileScannerException( 253 throw const MobileScannerException(
@@ -342,11 +345,13 @@ class MobileScannerController { @@ -342,11 +345,13 @@ class MobileScannerController {
342 ); 345 );
343 break; 346 break;
344 case 'barcodeWeb': 347 case 'barcodeWeb':
  348 + final barcode = data as Map?;
345 _barcodesController.add( 349 _barcodesController.add(
346 BarcodeCapture( 350 BarcodeCapture(
347 barcodes: [ 351 barcodes: [
348 Barcode( 352 Barcode(
349 - rawValue: data as String?, 353 + rawValue: barcode?['rawValue'] as String?,
  354 + rawBytes: barcode?['rawBytes'] as Uint8List?,
350 ) 355 )
351 ], 356 ],
352 ), 357 ),
@@ -12,6 +12,14 @@ class MobileScannerException implements Exception { @@ -12,6 +12,14 @@ class MobileScannerException implements Exception {
12 12
13 /// The additional error details that came with the [errorCode]. 13 /// The additional error details that came with the [errorCode].
14 final MobileScannerErrorDetails? errorDetails; 14 final MobileScannerErrorDetails? errorDetails;
  15 +
  16 + @override
  17 + String toString() {
  18 + if (errorDetails != null && errorDetails?.message != null) {
  19 + return "MobileScannerException: code ${errorCode.name}, message: ${errorDetails?.message}";
  20 + }
  21 + return "MobileScannerException: ${errorCode.name}";
  22 + }
15 } 23 }
16 24
17 /// The raw error details for a [MobileScannerException]. 25 /// The raw error details for a [MobileScannerException].
  1 +import 'dart:html' as html;
  2 +
  3 +import 'package:flutter/material.dart';
  4 +import 'package:js/js.dart';
  5 +import 'package:js/js_util.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/media.dart';
  9 +
  10 +abstract class WebBarcodeReaderBase {
  11 + /// Timer used to capture frames to be analyzed
  12 + final Duration frameInterval;
  13 + final html.DivElement videoContainer;
  14 +
  15 + const WebBarcodeReaderBase({
  16 + required this.videoContainer,
  17 + this.frameInterval = const Duration(milliseconds: 200),
  18 + });
  19 +
  20 + bool get isStarted;
  21 +
  22 + int get videoWidth;
  23 + int get videoHeight;
  24 +
  25 + /// Starts streaming video
  26 + Future<void> start({
  27 + required CameraFacing cameraFacing,
  28 + });
  29 +
  30 + /// Starts scanning QR codes or barcodes
  31 + Stream<Barcode?> detectBarcodeContinuously();
  32 +
  33 + /// Stops streaming video
  34 + Future<void> stop();
  35 +
  36 + /// Can enable or disable the flash if available
  37 + Future<void> toggleTorch({required bool enabled});
  38 +
  39 + /// Determine whether device has flash
  40 + Future<bool> hasTorch();
  41 +}
  42 +
  43 +mixin InternalStreamCreation on WebBarcodeReaderBase {
  44 + /// The video stream.
  45 + /// Will be initialized later to see which camera needs to be used.
  46 + html.MediaStream? localMediaStream;
  47 + final html.VideoElement video = html.VideoElement();
  48 +
  49 + @override
  50 + int get videoWidth => video.videoWidth;
  51 + @override
  52 + int get videoHeight => video.videoHeight;
  53 +
  54 + Future<html.MediaStream?> initMediaStream(CameraFacing cameraFacing) async {
  55 + // Check if browser supports multiple camera's and set if supported
  56 + final Map? capabilities =
  57 + html.window.navigator.mediaDevices?.getSupportedConstraints();
  58 + final Map<String, dynamic> constraints;
  59 + if (capabilities != null && capabilities['facingMode'] as bool) {
  60 + constraints = {
  61 + 'video': VideoOptions(
  62 + facingMode:
  63 + cameraFacing == CameraFacing.front ? 'user' : 'environment',
  64 + )
  65 + };
  66 + } else {
  67 + constraints = {'video': true};
  68 + }
  69 + final stream =
  70 + await html.window.navigator.mediaDevices?.getUserMedia(constraints);
  71 + return stream;
  72 + }
  73 +
  74 + void prepareVideoElement(html.VideoElement videoSource);
  75 +
  76 + Future<void> attachStreamToVideo(
  77 + html.MediaStream stream,
  78 + html.VideoElement videoSource,
  79 + );
  80 +
  81 + @override
  82 + Future<void> stop() async {
  83 + try {
  84 + // Stop the camera stream
  85 + localMediaStream?.getTracks().forEach((track) {
  86 + if (track.readyState == 'live') {
  87 + track.stop();
  88 + }
  89 + });
  90 + } catch (e) {
  91 + debugPrint('Failed to stop stream: $e');
  92 + }
  93 + video.srcObject = null;
  94 + localMediaStream = null;
  95 + videoContainer.children = [];
  96 + }
  97 +}
  98 +
  99 +/// Mixin for libraries that don't have built-in torch support
  100 +mixin InternalTorchDetection on InternalStreamCreation {
  101 + Future<List<String>> getSupportedTorchStates() async {
  102 + try {
  103 + final track = localMediaStream?.getVideoTracks();
  104 + if (track != null) {
  105 + final imageCapture = ImageCapture(track.first);
  106 + final photoCapabilities = await promiseToFuture<PhotoCapabilities>(
  107 + imageCapture.getPhotoCapabilities(),
  108 + );
  109 + final fillLightMode = photoCapabilities.fillLightMode;
  110 + if (fillLightMode != null) {
  111 + return fillLightMode;
  112 + }
  113 + }
  114 + } catch (e) {
  115 + // ImageCapture is not supported by some browsers:
  116 + // https://developer.mozilla.org/en-US/docs/Web/API/ImageCapture#browser_compatibility
  117 + }
  118 + return [];
  119 + }
  120 +
  121 + @override
  122 + Future<bool> hasTorch() async {
  123 + return (await getSupportedTorchStates()).isNotEmpty;
  124 + }
  125 +
  126 + @override
  127 + Future<void> toggleTorch({required bool enabled}) async {
  128 + final hasTorch = await this.hasTorch();
  129 + if (hasTorch) {
  130 + final track = localMediaStream?.getVideoTracks();
  131 + await track?.first.applyConstraints({
  132 + 'advanced': [
  133 + {'torch': enabled}
  134 + ]
  135 + });
  136 + }
  137 + }
  138 +}
  139 +
  140 +@JS('Promise')
  141 +@staticInterop
  142 +class Promise<T> {}
  143 +
  144 +@JS()
  145 +@anonymous
  146 +class PhotoCapabilities {
  147 + /// Returns an array of available fill light options. Options include auto, off, or flash.
  148 + external List<String>? get fillLightMode;
  149 +}
  150 +
  151 +@JS('ImageCapture')
  152 +@staticInterop
  153 +class ImageCapture {
  154 + /// MediaStreamTrack
  155 + external factory ImageCapture(dynamic track);
  156 +}
  157 +
  158 +extension ImageCaptureExt on ImageCapture {
  159 + external Promise<PhotoCapabilities> getPhotoCapabilities();
  160 +}
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 +}
1 -@JS()  
2 -library qrscanner;  
3 -  
4 -import 'package:js/js.dart';  
5 -  
6 -@JS('QrScanner')  
7 -external String scanImage(dynamic data);  
8 -  
9 -@JS()  
10 -class QrScanner {  
11 - external String get scanImage;  
12 -}  
  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('ZXing.BrowserMultiFormatReader')
  11 +@staticInterop
  12 +class JsZXingBrowserMultiFormatReader {
  13 + /// https://github.com/zxing-js/library/blob/1e9ccb3b6b28d75b9eef866dba196d8937eb4449/src/browser/BrowserMultiFormatReader.ts#L11
  14 + external factory JsZXingBrowserMultiFormatReader(
  15 + dynamic hints,
  16 + int timeBetweenScansMillis,
  17 + );
  18 +}
  19 +
  20 +@JS()
  21 +@anonymous
  22 +abstract class Result {
  23 + /// raw text encoded by the barcode
  24 + external String get text;
  25 +
  26 + /// Returns raw bytes encoded by the barcode, if applicable, otherwise null
  27 + external Uint8ClampedList? get rawBytes;
  28 +
  29 + /// Representing the format of the barcode that was decoded
  30 + external int? format;
  31 +}
  32 +
  33 +extension ResultExt on Result {
  34 + Barcode toBarcode() {
  35 + final rawBytes = this.rawBytes;
  36 + return Barcode(
  37 + rawValue: text,
  38 + rawBytes: rawBytes != null ? Uint8List.fromList(rawBytes) : null,
  39 + format: barcodeFormat,
  40 + );
  41 + }
  42 +
  43 + /// https://github.com/zxing-js/library/blob/1e9ccb3b6b28d75b9eef866dba196d8937eb4449/src/core/BarcodeFormat.ts#L28
  44 + BarcodeFormat get barcodeFormat {
  45 + switch (format) {
  46 + case 1:
  47 + return BarcodeFormat.aztec;
  48 + case 2:
  49 + return BarcodeFormat.codebar;
  50 + case 3:
  51 + return BarcodeFormat.code39;
  52 + case 4:
  53 + return BarcodeFormat.code128;
  54 + case 5:
  55 + return BarcodeFormat.dataMatrix;
  56 + case 6:
  57 + return BarcodeFormat.ean8;
  58 + case 7:
  59 + return BarcodeFormat.ean13;
  60 + case 8:
  61 + return BarcodeFormat.itf;
  62 + // case 9:
  63 + // return BarcodeFormat.maxicode;
  64 + case 10:
  65 + return BarcodeFormat.pdf417;
  66 + case 11:
  67 + return BarcodeFormat.qrCode;
  68 + // case 12:
  69 + // return BarcodeFormat.rss14;
  70 + // case 13:
  71 + // return BarcodeFormat.rssExp;
  72 + case 14:
  73 + return BarcodeFormat.upcA;
  74 + case 15:
  75 + return BarcodeFormat.upcE;
  76 + default:
  77 + return BarcodeFormat.unknown;
  78 + }
  79 + }
  80 +}
  81 +
  82 +typedef BarcodeDetectionCallback = void Function(
  83 + Result? result,
  84 + dynamic error,
  85 +);
  86 +
  87 +extension JsZXingBrowserMultiFormatReaderExt
  88 + on JsZXingBrowserMultiFormatReader {
  89 + external Promise<void> decodeFromVideoElementContinuously(
  90 + VideoElement source,
  91 + BarcodeDetectionCallback callbackFn,
  92 + );
  93 +
  94 + /// Continuously decodes from video input
  95 + external void decodeContinuously(
  96 + VideoElement element,
  97 + BarcodeDetectionCallback callbackFn,
  98 + );
  99 +
  100 + external Promise<void> decodeFromStream(
  101 + MediaStream stream,
  102 + VideoElement videoSource,
  103 + BarcodeDetectionCallback callbackFn,
  104 + );
  105 +
  106 + external Promise<void> decodeFromConstraints(
  107 + dynamic constraints,
  108 + VideoElement videoSource,
  109 + BarcodeDetectionCallback callbackFn,
  110 + );
  111 +
  112 + external void stopContinuousDecode();
  113 +
  114 + external VideoElement prepareVideoElement(VideoElement videoSource);
  115 +
  116 + /// Defines what the [videoElement] src will be.
  117 + external void addVideoSource(
  118 + VideoElement videoElement,
  119 + MediaStream stream,
  120 + );
  121 +
  122 + external bool isVideoPlaying(VideoElement video);
  123 +
  124 + external void reset();
  125 +
  126 + /// The HTML video element, used to display the camera stream.
  127 + external VideoElement? videoElement;
  128 +
  129 + /// The stream output from camera.
  130 + external MediaStream? stream;
  131 +}
  132 +
  133 +class ZXingBarcodeReader extends WebBarcodeReaderBase
  134 + with InternalStreamCreation, InternalTorchDetection {
  135 + late final JsZXingBrowserMultiFormatReader _reader =
  136 + JsZXingBrowserMultiFormatReader(
  137 + null,
  138 + frameInterval.inMilliseconds,
  139 + );
  140 +
  141 + ZXingBarcodeReader({required super.videoContainer});
  142 +
  143 + @override
  144 + bool get isStarted => localMediaStream != null;
  145 +
  146 + @override
  147 + Future<void> start({
  148 + required CameraFacing cameraFacing,
  149 + }) async {
  150 + videoContainer.children = [video];
  151 +
  152 + final stream = await initMediaStream(cameraFacing);
  153 +
  154 + prepareVideoElement(video);
  155 + if (stream != null) {
  156 + await attachStreamToVideo(stream, video);
  157 + }
  158 + }
  159 +
  160 + @override
  161 + void prepareVideoElement(VideoElement videoSource) {
  162 + _reader.prepareVideoElement(videoSource);
  163 + }
  164 +
  165 + @override
  166 + Future<void> attachStreamToVideo(
  167 + MediaStream stream,
  168 + VideoElement videoSource,
  169 + ) async {
  170 + _reader.addVideoSource(videoSource, stream);
  171 + _reader.videoElement = videoSource;
  172 + _reader.stream = stream;
  173 + localMediaStream = stream;
  174 + await videoSource.play();
  175 + }
  176 +
  177 + @override
  178 + Stream<Barcode?> detectBarcodeContinuously() {
  179 + final controller = StreamController<Barcode?>();
  180 + controller.onListen = () async {
  181 + _reader.decodeContinuously(
  182 + video,
  183 + allowInterop((result, error) {
  184 + if (result != null) {
  185 + controller.add(result.toBarcode());
  186 + }
  187 + }),
  188 + );
  189 + };
  190 + controller.onCancel = () {
  191 + _reader.stopContinuousDecode();
  192 + };
  193 + return controller.stream;
  194 + }
  195 +
  196 + @override
  197 + Future<void> stop() async {
  198 + _reader.reset();
  199 + super.stop();
  200 + }
  201 +}
@@ -18,7 +18,7 @@ dependencies: @@ -18,7 +18,7 @@ dependencies:
18 dev_dependencies: 18 dev_dependencies:
19 flutter_test: 19 flutter_test:
20 sdk: flutter 20 sdk: flutter
21 - lint: ^1.10.0 21 + lint: ">=1.10.0 <3.0.0"
22 22
23 flutter: 23 flutter:
24 plugin: 24 plugin: