Julian Steenbakker

refactor: web version of mobile_scanner

@@ -28,6 +28,7 @@ @@ -28,6 +28,7 @@
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>-->
31 <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script> 32 <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
32 </head> 33 </head>
33 <body> 34 <body>
1 { 1 {
2 - "name": "example",  
3 - "short_name": "example", 2 + "name": "Mobile Scanner Example",
  3 + "short_name": "mobile_scanner_example",
4 "start_url": ".", 4 "start_url": ".",
5 "display": "standalone", 5 "display": "standalone",
6 "background_color": "#0175C2", 6 "background_color": "#0175C2",
7 "theme_color": "#0175C2", 7 "theme_color": "#0175C2",
8 - "description": "A new Flutter project.", 8 + "description": "A barcode and qr code scanner example.",
9 "orientation": "portrait-primary", 9 "orientation": "portrait-primary",
10 "prefer_related_applications": false, 10 "prefer_related_applications": false,
11 "icons": [ 11 "icons": [
  1 +import 'package:flutter/foundation.dart';
1 import 'package:flutter/material.dart'; 2 import 'package:flutter/material.dart';
2 import 'package:mobile_scanner/mobile_scanner.dart'; 3 import 'package:mobile_scanner/mobile_scanner.dart';
3 4
@@ -67,7 +68,7 @@ class _MobileScannerState extends State<MobileScanner> @@ -67,7 +68,7 @@ class _MobileScannerState extends State<MobileScanner>
67 @override 68 @override
68 Widget build(BuildContext context) { 69 Widget build(BuildContext context) {
69 if (kIsWeb) { 70 if (kIsWeb) {
70 - return createWebQrView( 71 + return WebScanner(
71 onDetect: (barcode) => widget.onDetect!(barcode, null), 72 onDetect: (barcode) => widget.onDetect!(barcode, null),
72 cameraFacing: CameraFacing.back, 73 cameraFacing: CameraFacing.back,
73 ); 74 );
@@ -105,7 +106,6 @@ class _MobileScannerState extends State<MobileScanner> @@ -105,7 +106,6 @@ class _MobileScannerState extends State<MobileScanner>
105 ); 106 );
106 } 107 }
107 }); 108 });
108 - }  
109 }); 109 });
110 } 110 }
111 } 111 }
@@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart'; @@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart';
4 import 'package:flutter/services.dart'; 4 import 'package:flutter/services.dart';
5 import 'package:mobile_scanner/mobile_scanner.dart'; 5 import 'package:mobile_scanner/mobile_scanner.dart';
6 6
  7 +import 'mobile_scanner_arguments.dart';
7 import 'objects/barcode_utility.dart'; 8 import 'objects/barcode_utility.dart';
8 9
9 /// The facing of a camera. 10 /// The facing of a camera.
1 -// import 'package:flutter/material.dart';  
2 -// import 'package:qr_code_scanner/src/types/camera.dart';  
3 -//  
4 -// Widget createWebQrView({onPlatformViewCreated, CameraFacing? cameraFacing}) =>  
5 -// const SizedBox();  
@@ -3,7 +3,6 @@ @@ -3,7 +3,6 @@
3 import 'dart:async'; 3 import 'dart:async';
4 import 'dart:core'; 4 import 'dart:core';
5 import 'dart:html' as html; 5 import 'dart:html' as html;
6 -import 'dart:js_util';  
7 import 'dart:ui' as ui; 6 import 'dart:ui' as ui;
8 7
9 import 'package:flutter/material.dart'; 8 import 'package:flutter/material.dart';
@@ -18,21 +17,21 @@ import 'media.dart'; @@ -18,21 +17,21 @@ import 'media.dart';
18 /// Copyright 2020 @treeder 17 /// Copyright 2020 @treeder
19 /// Copyright 2021 The one with the braid 18 /// Copyright 2021 The one with the braid
20 19
21 -class WebQrView extends StatefulWidget { 20 +class WebScanner extends StatefulWidget {
22 final Function(Barcode) onDetect; 21 final Function(Barcode) onDetect;
23 final CameraFacing? cameraFacing; 22 final CameraFacing? cameraFacing;
24 23
25 - const WebQrView( 24 + const WebScanner(
26 {Key? key, 25 {Key? key,
27 required this.onDetect, 26 required this.onDetect,
28 this.cameraFacing = CameraFacing.front}) 27 this.cameraFacing = CameraFacing.front})
29 : super(key: key); 28 : super(key: key);
30 29
31 @override 30 @override
32 - _WebQrViewState createState() => _WebQrViewState(); 31 + _WebScannerState createState() => _WebScannerState();
33 32
34 - static html.DivElement vidDiv =  
35 - html.DivElement(); // need a global for the registerViewFactory 33 + // need a global for the registerViewFactory
  34 + static html.DivElement vidDiv = html.DivElement();
36 35
37 static Future<bool> cameraAvailable() async { 36 static Future<bool> cameraAvailable() async {
38 final sources = 37 final sources =
@@ -49,50 +48,65 @@ class WebQrView extends StatefulWidget { @@ -49,50 +48,65 @@ class WebQrView extends StatefulWidget {
49 } 48 }
50 } 49 }
51 50
52 -class _WebQrViewState extends State<WebQrView> { 51 +class _WebScannerState extends State<WebScanner> {
  52 + // Which way the camera is facing
  53 + // late CameraFacing facing;
  54 +
  55 + // The camera stream to display to the user
53 html.MediaStream? _localStream; 56 html.MediaStream? _localStream;
54 - // html.CanvasElement canvas;  
55 - // html.CanvasRenderingContext2D ctx; 57 +
  58 + // Check if analyzer is processing barcode
56 bool _currentlyProcessing = false; 59 bool _currentlyProcessing = false;
57 60
58 // QRViewControllerWeb? _controller; 61 // QRViewControllerWeb? _controller;
59 62
60 - late Size _size = const Size(0, 0); 63 + // Set size of the webview
  64 + // Size _size = const Size(0, 0);
  65 +
  66 + // TODO: Timer for capture?
61 Timer? timer; 67 Timer? timer;
62 - String? code; 68 +
  69 + // String? code;
  70 +
  71 + // TODO: Error message if error
63 String? _errorMsg; 72 String? _errorMsg;
  73 +
  74 + // Video element to be played on
64 html.VideoElement video = html.VideoElement(); 75 html.VideoElement video = html.VideoElement();
65 - String viewID = 'QRVIEW-' + DateTime.now().millisecondsSinceEpoch.toString();  
66 76
67 - final StreamController<Barcode> _scanUpdateController =  
68 - StreamController<Barcode>();  
69 - late CameraFacing facing; 77 + // ID of the video feed
  78 + String viewID =
  79 + 'WebScanner-' + DateTime.now().millisecondsSinceEpoch.toString();
  80 +
  81 + // final StreamController<Barcode> _scanUpdateController =
  82 + // StreamController<Barcode>();
70 83
  84 + // Timer for interval capture
71 Timer? _frameIntervall; 85 Timer? _frameIntervall;
72 86
73 @override 87 @override
74 void initState() { 88 void initState() {
75 super.initState(); 89 super.initState();
  90 + // facing = widget.cameraFacing ?? CameraFacing.front;
  91 + WebScanner.vidDiv.children = [video];
76 92
77 - facing = widget.cameraFacing ?? CameraFacing.front;  
78 -  
79 - // video = html.VideoElement();  
80 - WebQrView.vidDiv.children = [video];  
81 // ignore: UNDEFINED_PREFIXED_NAME 93 // ignore: UNDEFINED_PREFIXED_NAME
82 ui.platformViewRegistry 94 ui.platformViewRegistry
83 - .registerViewFactory(viewID, (int id) => WebQrView.vidDiv); 95 + .registerViewFactory(viewID, (int id) => WebScanner.vidDiv);
  96 +
84 // giving JavaScipt some time to process the DOM changes 97 // giving JavaScipt some time to process the DOM changes
85 Timer(const Duration(milliseconds: 500), () { 98 Timer(const Duration(milliseconds: 500), () {
86 start(); 99 start();
87 }); 100 });
88 } 101 }
89 102
  103 + /// Initialize camera and capture frame
90 Future start() async { 104 Future start() async {
91 - await _makeCall(); 105 + await _startVideoStream();
92 _frameIntervall?.cancel(); 106 _frameIntervall?.cancel();
93 _frameIntervall = 107 _frameIntervall =
94 Timer.periodic(const Duration(milliseconds: 200), (timer) { 108 Timer.periodic(const Duration(milliseconds: 200), (timer) {
95 - _captureFrame2(); 109 + _captureFrame();
96 }); 110 });
97 } 111 }
98 112
@@ -102,7 +116,7 @@ class _WebQrViewState extends State<WebQrView> { @@ -102,7 +116,7 @@ class _WebQrViewState extends State<WebQrView> {
102 timer = null; 116 timer = null;
103 } 117 }
104 if (_currentlyProcessing) { 118 if (_currentlyProcessing) {
105 - _stopStream(); 119 + _stopVideoStream();
106 } 120 }
107 } 121 }
108 122
@@ -112,30 +126,42 @@ class _WebQrViewState extends State<WebQrView> { @@ -112,30 +126,42 @@ class _WebQrViewState extends State<WebQrView> {
112 super.dispose(); 126 super.dispose();
113 } 127 }
114 128
115 - // Platform messages are asynchronous, so we initialize in an async method.  
116 - Future<void> _makeCall() async {  
117 - if (_localStream != null) {  
118 - return;  
119 - } 129 + /// Starts a video stream if not started already
  130 + Future<void> _startVideoStream() async {
  131 + // Check if stream is running
  132 + if (_localStream != null) return;
120 133
121 try { 134 try {
122 - var constraints = UserMediaOptions( 135 + // Check if browser supports multiple camera's and set if supported
  136 + Map? capabilities =
  137 + html.window.navigator.mediaDevices?.getSupportedConstraints();
  138 + if (capabilities != null && capabilities['facingMode']) {
  139 + UserMediaOptions constraints = UserMediaOptions(
123 video: VideoOptions( 140 video: VideoOptions(
124 - facingMode: (facing == CameraFacing.front ? 'user' : 'environment'), 141 + facingMode: (widget.cameraFacing == CameraFacing.front
  142 + ? 'user'
  143 + : 'environment'),
  144 + width: {'ideal': 4096},
  145 + height: {'ideal': 2160},
125 )); 146 ));
126 - // dart style, not working properly:  
127 - // var stream =  
128 - // await html.window.navigator.mediaDevices.getUserMedia(constraints);  
129 - // straight JS:  
130 - var stream = await promiseToFuture(getUserMedia(constraints));  
131 - _localStream = stream; 147 +
  148 + _localStream =
  149 + await html.window.navigator.getUserMedia(video: constraints);
  150 + } else {
  151 + _localStream = await html.window.navigator.getUserMedia(video: true);
  152 + }
  153 +
132 video.srcObject = _localStream; 154 video.srcObject = _localStream;
133 - video.setAttribute('playsinline',  
134 - 'true'); // required to tell iOS safari we don't want fullscreen 155 +
  156 + // required to tell iOS safari we don't want fullscreen
  157 + video.setAttribute('playsinline', 'true');
  158 +
  159 + // TODO: Check controller
135 // if (_controller == null) { 160 // if (_controller == null) {
136 // _controller = QRViewControllerWeb(this); 161 // _controller = QRViewControllerWeb(this);
137 // widget.onPlatformViewCreated(_controller!); 162 // widget.onPlatformViewCreated(_controller!);
138 // } 163 // }
  164 +
139 await video.play(); 165 await video.play();
140 } catch (e) { 166 } catch (e) {
141 cancel(); 167 cancel();
@@ -144,6 +170,7 @@ class _WebQrViewState extends State<WebQrView> { @@ -144,6 +170,7 @@ class _WebQrViewState extends State<WebQrView> {
144 }); 170 });
145 return; 171 return;
146 } 172 }
  173 +
147 if (!mounted) return; 174 if (!mounted) return;
148 175
149 setState(() { 176 setState(() {
@@ -151,45 +178,43 @@ class _WebQrViewState extends State<WebQrView> { @@ -151,45 +178,43 @@ class _WebQrViewState extends State<WebQrView> {
151 }); 178 });
152 } 179 }
153 180
154 - Future<void> _stopStream() async { 181 + Future<void> _stopVideoStream() async {
155 try { 182 try {
156 - // await _localStream.dispose(); 183 + // Stop the camera stream
157 _localStream!.getTracks().forEach((track) { 184 _localStream!.getTracks().forEach((track) {
158 if (track.readyState == 'live') { 185 if (track.readyState == 'live') {
159 track.stop(); 186 track.stop();
160 } 187 }
161 }); 188 });
162 - // video.stop(); 189 +
163 video.srcObject = null; 190 video.srcObject = null;
164 _localStream = null; 191 _localStream = null;
165 - // _localRenderer.srcObject = null;  
166 - // ignore: empty_catches  
167 - } catch (e) {} 192 + } catch (e) {
  193 + debugPrint('Failed to stop stream: $e');
168 } 194 }
169 -  
170 - Future<dynamic> _captureFrame2() async {  
171 - if (_localStream == null) {  
172 - return null;  
173 } 195 }
174 - final canvas =  
175 - html.CanvasElement(width: video.videoWidth, height: video.videoHeight); 196 +
  197 + Future<dynamic> _captureFrame() async {
  198 + if (_localStream == null) return null;
  199 + final canvas = html.CanvasElement(width: video.videoWidth, height: video.videoHeight);
176 final ctx = canvas.context2D; 200 final ctx = canvas.context2D;
177 - // canvas.width = video.videoWidth;  
178 - // canvas.height = video.videoHeight; 201 +
179 ctx.drawImage(video, 0, 0); 202 ctx.drawImage(video, 0, 0);
180 final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!); 203 final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!);
181 204
182 - final size =  
183 - Size(canvas.width?.toDouble() ?? 0, canvas.height?.toDouble() ?? 0);  
184 - if (size != _size) {  
185 - setState(() {  
186 - _setCanvasSize(size);  
187 - });  
188 - } 205 + // final size =
  206 + // Size(canvas.width?.toDouble() ?? 0, canvas.height?.toDouble() ?? 0);
  207 + // if (size != _size) {
  208 + // setState(() {
  209 + // _setCanvasSize(size);
  210 + // });
  211 + // }
  212 + // debugPrint('img.data: ${imgData.data}');
189 final code = jsQR(imgData.data, canvas.width, canvas.height); 213 final code = jsQR(imgData.data, canvas.width, canvas.height);
190 // ignore: unnecessary_null_comparison 214 // ignore: unnecessary_null_comparison
191 if (code != null) { 215 if (code != null) {
192 - widget.onDetect(Barcode(rawValue: code.data)); 216 + debugPrint('CODE: $code');
  217 + // widget.onDetect(Barcode(rawValue: code.data));
193 // print('Barcode: ${code.data}'); 218 // print('Barcode: ${code.data}');
194 // _scanUpdateController 219 // _scanUpdateController
195 // .add(Barcode(rawValue: code.data)); 220 // .add(Barcode(rawValue: code.data));
@@ -204,118 +229,14 @@ class _WebQrViewState extends State<WebQrView> { @@ -204,118 +229,14 @@ class _WebQrViewState extends State<WebQrView> {
204 if (_localStream == null) { 229 if (_localStream == null) {
205 return const Center(child: CircularProgressIndicator()); 230 return const Center(child: CircularProgressIndicator());
206 } 231 }
207 - return LayoutBuilder(  
208 - builder: (context, constraints) {  
209 - var zoom = 1.0;  
210 -  
211 - if (_size.height != 0) zoom = constraints.maxHeight / _size.height;  
212 -  
213 - if (_size.width != 0) {  
214 - final horizontalZoom = constraints.maxWidth / _size.width;  
215 - if (horizontalZoom > zoom) {  
216 - zoom = horizontalZoom;  
217 - }  
218 - }  
219 232
220 return SizedBox( 233 return SizedBox(
221 - width: constraints.maxWidth,  
222 - height: constraints.maxHeight,  
223 - child: Center(  
224 - child: SizedBox.fromSize(  
225 - size: _size,  
226 - child: Transform.scale(  
227 - alignment: Alignment.center,  
228 - scale: zoom,  
229 - child: HtmlElementView(viewType: viewID),  
230 - ),  
231 - ),  
232 - ),  
233 - );  
234 - },  
235 - );  
236 - }  
237 -  
238 - void _setCanvasSize(ui.Size size) {  
239 - setState(() {  
240 - _size = size;  
241 - }); 234 + width: MediaQuery.of(context).size.width,
  235 + height: MediaQuery.of(context).size.height,
  236 + child: FittedBox(
  237 + child: SizedBox(
  238 + width: video.videoWidth.toDouble(),
  239 + height: video.videoHeight.toDouble(),
  240 + child: HtmlElementView(viewType: viewID))));
242 } 241 }
243 } 242 }
244 -//  
245 -// class QRViewControllerWeb implements QRViewController {  
246 -// final _WebQrViewState _state;  
247 -//  
248 -// QRViewControllerWeb(this._state);  
249 -// @override  
250 -// void dispose() => _state.cancel();  
251 -//  
252 -// @override  
253 -// Future<CameraFacing> flipCamera() async {  
254 -// // TODO: improve error handling  
255 -// _state.facing = _state.facing == CameraFacing.front  
256 -// ? CameraFacing.back  
257 -// : CameraFacing.front;  
258 -// await _state.start();  
259 -// return _state.facing;  
260 -// }  
261 -//  
262 -// @override  
263 -// Future<CameraFacing> getCameraInfo() async {  
264 -// return _state.facing;  
265 -// }  
266 -//  
267 -// @override  
268 -// Future<bool?> getFlashStatus() async {  
269 -// // TODO: flash is simply not supported by JavaScipt. To avoid issuing applications, we always return it to be off.  
270 -// return false;  
271 -// }  
272 -//  
273 -// @override  
274 -// Future<SystemFeatures> getSystemFeatures() {  
275 -// // TODO: implement getSystemFeatures  
276 -// throw UnimplementedError();  
277 -// }  
278 -//  
279 -// @override  
280 -// // TODO: implement hasPermissions. Blocking: WebQrView.cameraAvailable() returns a Future<bool> whereas a bool is required  
281 -// bool get hasPermissions => throw UnimplementedError();  
282 -//  
283 -// @override  
284 -// Future<void> pauseCamera() {  
285 -// // TODO: implement pauseCamera  
286 -// throw UnimplementedError();  
287 -// }  
288 -//  
289 -// @override  
290 -// Future<void> resumeCamera() {  
291 -// // TODO: implement resumeCamera  
292 -// throw UnimplementedError();  
293 -// }  
294 -//  
295 -// @override  
296 -// Stream<Barcode> get scannedDataStream => _state._scanUpdateController.stream;  
297 -//  
298 -// @override  
299 -// Future<void> stopCamera() {  
300 -// // TODO: implement stopCamera  
301 -// throw UnimplementedError();  
302 -// }  
303 -//  
304 -// @override  
305 -// Future<void> toggleFlash() async {  
306 -// // TODO: flash is simply not supported by JavaScipt  
307 -// return;  
308 -// }  
309 -//  
310 -// @override  
311 -// Future<void> scanInvert(bool isScanInvert) {  
312 -// // TODO: implement scanInvert  
313 -// throw UnimplementedError();  
314 -// }  
315 -// }  
316 -  
317 -Widget createWebQrView({required Function(Barcode) onDetect, CameraFacing? cameraFacing}) =>  
318 - WebQrView(  
319 - onDetect: onDetect,  
320 - cameraFacing: cameraFacing,  
321 - );  
1 -// This is here because dart doesn't seem to support this properly  
2 -// https://stackoverflow.com/questions/61161135/adding-support-for-navigator-mediadevices-getusermedia-to-dart 1 +// // This is here because dart doesn't seem to support this properly
  2 +// // https://stackoverflow.com/questions/61161135/adding-support-for-navigator-mediadevices-getusermedia-to-dart
3 3
4 @JS('navigator.mediaDevices') 4 @JS('navigator.mediaDevices')
5 library media_devices; 5 library media_devices;
@@ -22,9 +22,11 @@ class UserMediaOptions { @@ -22,9 +22,11 @@ class UserMediaOptions {
22 class VideoOptions { 22 class VideoOptions {
23 external String get facingMode; 23 external String get facingMode;
24 // external DeviceIdOptions get deviceId; 24 // external DeviceIdOptions get deviceId;
  25 + external Map get width;
  26 + external Map get height;
25 27
26 external factory VideoOptions( 28 external factory VideoOptions(
27 - {String? facingMode, DeviceIdOptions? deviceId}); 29 + {String? facingMode, DeviceIdOptions? deviceId, Map? width, Map? height});
28 } 30 }
29 31
30 @JS() 32 @JS()
  1 +@JS()
  2 +library qrscanner;
  3 +
  4 +import 'package:js/js.dart';
  5 +
  6 +@JS('QrScanner')
  7 +external String scanImage(var data);
  8 +
  9 +@JS()
  10 +class QrScanner {
  11 + external String get scanImage;
  12 +}