Showing
3 changed files
with
124 additions
and
82 deletions
| @@ -33,10 +33,6 @@ class MobileScannerWebPlugin { | @@ -33,10 +33,6 @@ class MobileScannerWebPlugin { | ||
| 33 | // Controller to send events back to the framework | 33 | // Controller to send events back to the framework |
| 34 | StreamController controller = StreamController.broadcast(); | 34 | StreamController controller = StreamController.broadcast(); |
| 35 | 35 | ||
| 36 | - // The video stream. Will be initialized later to see which camera needs to be used. | ||
| 37 | - html.MediaStream? _localStream; | ||
| 38 | - html.VideoElement video = html.VideoElement(); | ||
| 39 | - | ||
| 40 | // ID of the video feed | 36 | // ID of the video feed |
| 41 | String viewID = 'WebScanner-${DateTime.now().millisecondsSinceEpoch}'; | 37 | String viewID = 'WebScanner-${DateTime.now().millisecondsSinceEpoch}'; |
| 42 | 38 | ||
| @@ -46,8 +42,6 @@ class MobileScannerWebPlugin { | @@ -46,8 +42,6 @@ class MobileScannerWebPlugin { | ||
| 46 | final WebBarcodeReaderBase _barCodeReader = JsQrCodeReader(); | 42 | final WebBarcodeReaderBase _barCodeReader = JsQrCodeReader(); |
| 47 | StreamSubscription? _barCodeStreamSubscription; | 43 | StreamSubscription? _barCodeStreamSubscription; |
| 48 | 44 | ||
| 49 | - html.DivElement vidDiv = html.DivElement(); | ||
| 50 | - | ||
| 51 | /// Handle incomming messages | 45 | /// Handle incomming messages |
| 52 | Future<dynamic> handleMethodCall(MethodCall call) async { | 46 | Future<dynamic> handleMethodCall(MethodCall call) async { |
| 53 | switch (call.method) { | 47 | switch (call.method) { |
| @@ -68,87 +62,48 @@ class MobileScannerWebPlugin { | @@ -68,87 +62,48 @@ class MobileScannerWebPlugin { | ||
| 68 | 62 | ||
| 69 | /// Can enable or disable the flash if available | 63 | /// Can enable or disable the flash if available |
| 70 | Future<void> _torch(arguments) async { | 64 | Future<void> _torch(arguments) async { |
| 71 | - if (hasFlash) { | ||
| 72 | - final track = _localStream?.getVideoTracks(); | ||
| 73 | - await track!.first.applyConstraints({ | ||
| 74 | - 'advanced': {'torch': arguments == 1} | ||
| 75 | - }); | ||
| 76 | - } else { | 65 | + // if (hasFlash) { |
| 66 | + // final track = _localStream?.getVideoTracks(); | ||
| 67 | + // await track!.first.applyConstraints({ | ||
| 68 | + // 'advanced': {'torch': arguments == 1} | ||
| 69 | + // }); | ||
| 70 | + // } else { | ||
| 71 | + // controller.addError('Device has no flash'); | ||
| 72 | + // } | ||
| 77 | controller.addError('Device has no flash'); | 73 | controller.addError('Device has no flash'); |
| 78 | } | 74 | } |
| 79 | - } | ||
| 80 | 75 | ||
| 81 | /// Starts the video stream and the scanner | 76 | /// Starts the video stream and the scanner |
| 82 | Future<Map> _start(Map arguments) async { | 77 | Future<Map> _start(Map arguments) async { |
| 83 | - vidDiv.children = [video]; | ||
| 84 | - | ||
| 85 | var cameraFacing = CameraFacing.front; | 78 | var cameraFacing = CameraFacing.front; |
| 86 | if (arguments.containsKey('facing')) { | 79 | if (arguments.containsKey('facing')) { |
| 87 | cameraFacing = CameraFacing.values[arguments['facing'] as int]; | 80 | cameraFacing = CameraFacing.values[arguments['facing'] as int]; |
| 88 | } | 81 | } |
| 89 | 82 | ||
| 90 | - // See https://github.com/flutter/flutter/issues/41563 | ||
| 91 | - // ignore: UNDEFINED_PREFIXED_NAME, avoid_dynamic_calls | ||
| 92 | - ui.platformViewRegistry.registerViewFactory( | ||
| 93 | - viewID, | ||
| 94 | - (int id) => vidDiv | ||
| 95 | - ..style.width = '100%' | ||
| 96 | - ..style.height = '100%', | ||
| 97 | - ); | ||
| 98 | - | ||
| 99 | // Check if stream is running | 83 | // Check if stream is running |
| 100 | - if (_localStream != null) { | 84 | + if (_barCodeReader.isStarted) { |
| 101 | return { | 85 | return { |
| 102 | 'ViewID': viewID, | 86 | 'ViewID': viewID, |
| 103 | - 'videoWidth': video.videoWidth, | ||
| 104 | - 'videoHeight': video.videoHeight | 87 | + 'videoWidth': _barCodeReader.videoWidth, |
| 88 | + 'videoHeight': _barCodeReader.videoHeight | ||
| 105 | }; | 89 | }; |
| 106 | } | 90 | } |
| 107 | - | ||
| 108 | try { | 91 | try { |
| 109 | - // Check if browser supports multiple camera's and set if supported | ||
| 110 | - final Map? capabilities = | ||
| 111 | - html.window.navigator.mediaDevices?.getSupportedConstraints(); | ||
| 112 | - if (capabilities != null && capabilities['facingMode'] as bool) { | ||
| 113 | - final constraints = { | ||
| 114 | - 'video': VideoOptions( | ||
| 115 | - facingMode: | ||
| 116 | - cameraFacing == CameraFacing.front ? 'user' : 'environment', | ||
| 117 | - ) | ||
| 118 | - }; | ||
| 119 | - | ||
| 120 | - _localStream = | ||
| 121 | - await html.window.navigator.mediaDevices?.getUserMedia(constraints); | ||
| 122 | - } else { | ||
| 123 | - _localStream = await html.window.navigator.mediaDevices | ||
| 124 | - ?.getUserMedia({'video': true}); | ||
| 125 | - } | ||
| 126 | - | ||
| 127 | - video.srcObject = _localStream; | ||
| 128 | - | ||
| 129 | - // TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533 | ||
| 130 | - // final track = _localStream?.getVideoTracks(); | ||
| 131 | - // if (track != null) { | ||
| 132 | - // final imageCapture = html.ImageCapture(track.first); | ||
| 133 | - // final photoCapabilities = await imageCapture.getPhotoCapabilities(); | ||
| 134 | - // } | ||
| 135 | - | ||
| 136 | - // required to tell iOS safari we don't want fullscreen | ||
| 137 | - video.setAttribute('playsinline', 'true'); | 92 | + await _barCodeReader.start( |
| 93 | + viewID: viewID, | ||
| 94 | + cameraFacing: cameraFacing, | ||
| 95 | + ); | ||
| 138 | 96 | ||
| 139 | - _barCodeStreamSubscription = | ||
| 140 | - _barCodeReader.detectBarcodeContinuously(video).listen((code) { | ||
| 141 | - if (_localStream == null) return; | 97 | + _barCodeStreamSubscription = _barCodeReader.detectBarcodeContinuously().listen((code) { |
| 142 | if (code != null) { | 98 | if (code != null) { |
| 143 | controller.add({'name': 'barcodeWeb', 'data': code}); | 99 | controller.add({'name': 'barcodeWeb', 'data': code}); |
| 144 | } | 100 | } |
| 145 | }); | 101 | }); |
| 146 | - await video.play(); | ||
| 147 | 102 | ||
| 148 | return { | 103 | return { |
| 149 | 'ViewID': viewID, | 104 | 'ViewID': viewID, |
| 150 | - 'videoWidth': video.videoWidth, | ||
| 151 | - 'videoHeight': video.videoHeight, | 105 | + 'videoWidth': _barCodeReader.videoWidth, |
| 106 | + 'videoHeight': _barCodeReader.videoHeight, | ||
| 152 | 'torchable': hasFlash | 107 | 'torchable': hasFlash |
| 153 | }; | 108 | }; |
| 154 | } catch (e) { | 109 | } catch (e) { |
| @@ -172,19 +127,7 @@ class MobileScannerWebPlugin { | @@ -172,19 +127,7 @@ class MobileScannerWebPlugin { | ||
| 172 | 127 | ||
| 173 | /// Stops the video feed and analyzer | 128 | /// Stops the video feed and analyzer |
| 174 | Future<void> cancel() async { | 129 | Future<void> cancel() async { |
| 175 | - try { | ||
| 176 | - // Stop the camera stream | ||
| 177 | - _localStream?.getTracks().forEach((track) { | ||
| 178 | - if (track.readyState == 'live') { | ||
| 179 | - track.stop(); | ||
| 180 | - } | ||
| 181 | - }); | ||
| 182 | - } catch (e) { | ||
| 183 | - debugPrint('Failed to stop stream: $e'); | ||
| 184 | - } | ||
| 185 | - | ||
| 186 | - video.srcObject = null; | ||
| 187 | - _localStream = null; | 130 | + _barCodeReader.stop(); |
| 188 | await _barCodeStreamSubscription?.cancel(); | 131 | await _barCodeStreamSubscription?.cancel(); |
| 189 | _barCodeStreamSubscription = null; | 132 | _barCodeStreamSubscription = null; |
| 190 | } | 133 | } |
| 1 | -import 'dart:html'; | 1 | +import 'package:mobile_scanner/src/enums/camera_facing.dart'; |
| 2 | 2 | ||
| 3 | abstract class WebBarcodeReaderBase { | 3 | abstract class WebBarcodeReaderBase { |
| 4 | - Stream<String?> detectBarcodeContinuously(VideoElement video); | 4 | + /// Timer used to capture frames to be analyzed |
| 5 | + final frameInterval = const Duration(milliseconds: 200); | ||
| 6 | + | ||
| 7 | + bool get isStarted; | ||
| 8 | + | ||
| 9 | + int get videoWidth; | ||
| 10 | + int get videoHeight; | ||
| 11 | + | ||
| 12 | + /// Starts streaming video | ||
| 13 | + Future<void> start({ | ||
| 14 | + required String viewID, | ||
| 15 | + required CameraFacing cameraFacing, | ||
| 16 | + }); | ||
| 17 | + | ||
| 18 | + /// Starts scanning QR codes or barcodes | ||
| 19 | + Stream<String?> detectBarcodeContinuously(); | ||
| 20 | + | ||
| 21 | + /// Stops streaming video | ||
| 22 | + Future<void> stop(); | ||
| 5 | } | 23 | } |
| @@ -4,10 +4,15 @@ library jsqr; | @@ -4,10 +4,15 @@ library jsqr; | ||
| 4 | import 'dart:async'; | 4 | import 'dart:async'; |
| 5 | import 'dart:html'; | 5 | import 'dart:html'; |
| 6 | import 'dart:typed_data'; | 6 | import 'dart:typed_data'; |
| 7 | +import 'dart:ui' as ui; | ||
| 7 | 8 | ||
| 9 | +import 'package:flutter/widgets.dart'; | ||
| 8 | import 'package:js/js.dart'; | 10 | import 'package:js/js.dart'; |
| 11 | +import 'package:mobile_scanner/src/enums/camera_facing.dart'; | ||
| 9 | import 'package:mobile_scanner/src/web/base.dart'; | 12 | import 'package:mobile_scanner/src/web/base.dart'; |
| 10 | 13 | ||
| 14 | +import 'media.dart'; | ||
| 15 | + | ||
| 11 | @JS('jsQR') | 16 | @JS('jsQR') |
| 12 | external Code? jsQR(dynamic data, int? width, int? height); | 17 | external Code? jsQR(dynamic data, int? width, int? height); |
| 13 | 18 | ||
| @@ -20,18 +25,77 @@ class Code { | @@ -20,18 +25,77 @@ class Code { | ||
| 20 | 25 | ||
| 21 | 26 | ||
| 22 | class JsQrCodeReader extends WebBarcodeReaderBase { | 27 | class JsQrCodeReader extends WebBarcodeReaderBase { |
| 23 | - // Timer used to capture frames to be analyzed | ||
| 24 | - final frameInterval = const Duration(milliseconds: 200); | 28 | + // The video stream. Will be initialized later to see which camera needs to be used. |
| 29 | + MediaStream? _localStream; | ||
| 30 | + | ||
| 31 | + VideoElement video = VideoElement(); | ||
| 32 | + | ||
| 33 | + DivElement vidDiv = DivElement(); | ||
| 34 | + | ||
| 35 | + @override | ||
| 36 | + bool get isStarted => _localStream != null; | ||
| 37 | + | ||
| 38 | + @override | ||
| 39 | + int get videoWidth => video.width; | ||
| 40 | + @override | ||
| 41 | + int get videoHeight => video.height; | ||
| 42 | + | ||
| 43 | + @override | ||
| 44 | + Future<void> start({ | ||
| 45 | + required String viewID, | ||
| 46 | + required CameraFacing cameraFacing, | ||
| 47 | + }) async { | ||
| 48 | + vidDiv.children = [video]; | ||
| 49 | + // See https://github.com/flutter/flutter/issues/41563 | ||
| 50 | + // ignore: UNDEFINED_PREFIXED_NAME, avoid_dynamic_calls | ||
| 51 | + ui.platformViewRegistry.registerViewFactory( | ||
| 52 | + viewID, | ||
| 53 | + (int id) => vidDiv | ||
| 54 | + ..style.width = '100%' | ||
| 55 | + ..style.height = '100%', | ||
| 56 | + ); | ||
| 57 | + // Check if browser supports multiple camera's and set if supported | ||
| 58 | + final Map? capabilities = | ||
| 59 | + window.navigator.mediaDevices?.getSupportedConstraints(); | ||
| 60 | + if (capabilities != null && capabilities['facingMode'] as bool) { | ||
| 61 | + final constraints = { | ||
| 62 | + 'video': VideoOptions( | ||
| 63 | + facingMode: | ||
| 64 | + cameraFacing == CameraFacing.front ? 'user' : 'environment', | ||
| 65 | + ) | ||
| 66 | + }; | ||
| 67 | + | ||
| 68 | + _localStream = | ||
| 69 | + await window.navigator.mediaDevices?.getUserMedia(constraints); | ||
| 70 | + } else { | ||
| 71 | + _localStream = await window.navigator.mediaDevices | ||
| 72 | + ?.getUserMedia({'video': true}); | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + video.srcObject = _localStream; | ||
| 76 | + | ||
| 77 | + // TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533 | ||
| 78 | + // final track = _localStream?.getVideoTracks(); | ||
| 79 | + // if (track != null) { | ||
| 80 | + // final imageCapture = html.ImageCapture(track.first); | ||
| 81 | + // final photoCapabilities = await imageCapture.getPhotoCapabilities(); | ||
| 82 | + // } | ||
| 83 | + | ||
| 84 | + // required to tell iOS safari we don't want fullscreen | ||
| 85 | + video.setAttribute('playsinline', 'true'); | ||
| 86 | + await video.play(); | ||
| 87 | + } | ||
| 25 | 88 | ||
| 26 | @override | 89 | @override |
| 27 | - Stream<String?> detectBarcodeContinuously(VideoElement video) async* { | 90 | + Stream<String?> detectBarcodeContinuously() async* { |
| 28 | yield* Stream.periodic(frameInterval, (_) { | 91 | yield* Stream.periodic(frameInterval, (_) { |
| 29 | return _captureFrame(video); | 92 | return _captureFrame(video); |
| 30 | - }).asyncMap((event) async => (await event)?.data); | 93 | + }).asyncMap((e) => e).map((event) => event?.data); |
| 31 | } | 94 | } |
| 32 | 95 | ||
| 33 | /// Captures a frame and analyzes it for QR codes | 96 | /// Captures a frame and analyzes it for QR codes |
| 34 | Future<Code?> _captureFrame(VideoElement video) async { | 97 | Future<Code?> _captureFrame(VideoElement video) async { |
| 98 | + if (_localStream == null) return null; | ||
| 35 | final canvas = CanvasElement(width: video.videoWidth, height: video.videoHeight); | 99 | final canvas = CanvasElement(width: video.videoWidth, height: video.videoHeight); |
| 36 | final ctx = canvas.context2D; | 100 | final ctx = canvas.context2D; |
| 37 | 101 | ||
| @@ -41,4 +105,21 @@ class JsQrCodeReader extends WebBarcodeReaderBase { | @@ -41,4 +105,21 @@ class JsQrCodeReader extends WebBarcodeReaderBase { | ||
| 41 | final code = jsQR(imgData.data, canvas.width, canvas.height); | 105 | final code = jsQR(imgData.data, canvas.width, canvas.height); |
| 42 | return code; | 106 | return code; |
| 43 | } | 107 | } |
| 108 | + | ||
| 109 | + @override | ||
| 110 | + Future<void> stop() async { | ||
| 111 | + try { | ||
| 112 | + // Stop the camera stream | ||
| 113 | + _localStream?.getTracks().forEach((track) { | ||
| 114 | + if (track.readyState == 'live') { | ||
| 115 | + track.stop(); | ||
| 116 | + } | ||
| 117 | + }); | ||
| 118 | + } catch (e) { | ||
| 119 | + debugPrint('Failed to stop stream: $e'); | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + video.srcObject = null; | ||
| 123 | + _localStream = null; | ||
| 124 | + } | ||
| 44 | } | 125 | } |
-
Please register or login to post a comment