Showing
7 changed files
with
245 additions
and
275 deletions
lib/mobile_scanner_web_plugin.dart
0 → 100644
| 1 | +import 'dart:async'; | ||
| 2 | + | ||
| 3 | +import 'package:flutter/material.dart'; | ||
| 4 | +import 'package:flutter/services.dart'; | ||
| 5 | +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; | ||
| 6 | +import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 7 | +import 'package:mobile_scanner/src/web/jsqr.dart'; | ||
| 8 | +import 'dart:html' as html; | ||
| 9 | +import 'dart:ui' as ui; | ||
| 10 | + | ||
| 11 | +import 'package:mobile_scanner/src/web/media.dart'; | ||
| 12 | + | ||
| 13 | +/// This plugin is the web implementation of mobile_scanner. | ||
| 14 | +/// It only supports QR codes. | ||
| 15 | +class MobileScannerWebPlugin { | ||
| 16 | + static void registerWith(Registrar registrar) { | ||
| 17 | + PluginEventChannel event = PluginEventChannel( | ||
| 18 | + 'dev.steenbakker.mobile_scanner/scanner/event', | ||
| 19 | + const StandardMethodCodec(), | ||
| 20 | + registrar); | ||
| 21 | + MethodChannel channel = MethodChannel( | ||
| 22 | + 'dev.steenbakker.mobile_scanner/scanner/method', | ||
| 23 | + const StandardMethodCodec(), | ||
| 24 | + registrar); | ||
| 25 | + final MobileScannerWebPlugin instance = MobileScannerWebPlugin(); | ||
| 26 | + WidgetsFlutterBinding.ensureInitialized(); | ||
| 27 | + | ||
| 28 | + channel.setMethodCallHandler(instance.handleMethodCall); | ||
| 29 | + event.setController(instance.controller); | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + // Controller to send events back to the framework | ||
| 33 | + StreamController controller = StreamController(); | ||
| 34 | + | ||
| 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 | ||
| 40 | + String viewID = 'WebScanner-${DateTime.now().millisecondsSinceEpoch}'; | ||
| 41 | + | ||
| 42 | + // Determine wether device has flas | ||
| 43 | + bool hasFlash = false; | ||
| 44 | + | ||
| 45 | + // Timer used to capture frames to be analyzed | ||
| 46 | + Timer? _frameInterval; | ||
| 47 | + | ||
| 48 | + html.DivElement vidDiv = html.DivElement(); | ||
| 49 | + | ||
| 50 | + /// Handle incomming messages | ||
| 51 | + Future<dynamic> handleMethodCall(MethodCall call) async { | ||
| 52 | + switch (call.method) { | ||
| 53 | + case 'start': | ||
| 54 | + return await _start(call.arguments); | ||
| 55 | + case 'torch': | ||
| 56 | + return await _torch(call.arguments); | ||
| 57 | + case 'stop': | ||
| 58 | + return await cancel(); | ||
| 59 | + default: | ||
| 60 | + throw PlatformException( | ||
| 61 | + code: 'Unimplemented', | ||
| 62 | + details: "The mobile_scanner plugin for web doesn't implement " | ||
| 63 | + "the method '${call.method}'"); | ||
| 64 | + } | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + /// Can enable or disable the flash if available | ||
| 68 | + Future<void> _torch(arguments) async { | ||
| 69 | + if (hasFlash) { | ||
| 70 | + final track = _localStream?.getVideoTracks(); | ||
| 71 | + await track!.first.applyConstraints({ | ||
| 72 | + 'advanced': {'torch': arguments == 1} | ||
| 73 | + }); | ||
| 74 | + } else { | ||
| 75 | + controller.addError('Device has no flash'); | ||
| 76 | + } | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + /// Starts the video stream and the scanner | ||
| 80 | + Future<Map> _start(arguments) async { | ||
| 81 | + vidDiv.children = [video]; | ||
| 82 | + | ||
| 83 | + final CameraFacing cameraFacing = | ||
| 84 | + arguments['cameraFacing'] ?? CameraFacing.front; | ||
| 85 | + | ||
| 86 | + // See https://github.com/flutter/flutter/issues/41563 | ||
| 87 | + // ignore: UNDEFINED_PREFIXED_NAME | ||
| 88 | + ui.platformViewRegistry.registerViewFactory( | ||
| 89 | + viewID, | ||
| 90 | + (int id) => vidDiv | ||
| 91 | + ..style.width = '100%' | ||
| 92 | + ..style.height = '100%'); | ||
| 93 | + | ||
| 94 | + // Check if stream is running | ||
| 95 | + if (_localStream != null) { | ||
| 96 | + return { | ||
| 97 | + 'ViewID': viewID, | ||
| 98 | + 'videoWidth': video.videoWidth, | ||
| 99 | + 'videoHeight': video.videoHeight | ||
| 100 | + }; | ||
| 101 | + } | ||
| 102 | + | ||
| 103 | + try { | ||
| 104 | + // Check if browser supports multiple camera's and set if supported | ||
| 105 | + Map? capabilities = | ||
| 106 | + html.window.navigator.mediaDevices?.getSupportedConstraints(); | ||
| 107 | + if (capabilities != null && capabilities['facingMode']) { | ||
| 108 | + UserMediaOptions constraints = UserMediaOptions( | ||
| 109 | + video: VideoOptions( | ||
| 110 | + facingMode: | ||
| 111 | + (cameraFacing == CameraFacing.front ? 'user' : 'environment'), | ||
| 112 | + width: {'ideal': 4096}, | ||
| 113 | + height: {'ideal': 2160}, | ||
| 114 | + )); | ||
| 115 | + | ||
| 116 | + _localStream = | ||
| 117 | + await html.window.navigator.getUserMedia(video: constraints); | ||
| 118 | + } else { | ||
| 119 | + _localStream = await html.window.navigator.getUserMedia(video: true); | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + video.srcObject = _localStream; | ||
| 123 | + | ||
| 124 | + // TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533 | ||
| 125 | + // final track = _localStream?.getVideoTracks(); | ||
| 126 | + // if (track != null) { | ||
| 127 | + // final imageCapture = html.ImageCapture(track.first); | ||
| 128 | + // final photoCapabilities = await imageCapture.getPhotoCapabilities(); | ||
| 129 | + // } | ||
| 130 | + | ||
| 131 | + // required to tell iOS safari we don't want fullscreen | ||
| 132 | + video.setAttribute('playsinline', 'true'); | ||
| 133 | + | ||
| 134 | + await video.play(); | ||
| 135 | + | ||
| 136 | + // Then capture a frame to be analyzed every 200 miliseconds | ||
| 137 | + _frameInterval = | ||
| 138 | + Timer.periodic(const Duration(milliseconds: 200), (timer) { | ||
| 139 | + _captureFrame(); | ||
| 140 | + }); | ||
| 141 | + | ||
| 142 | + return { | ||
| 143 | + 'ViewID': viewID, | ||
| 144 | + 'videoWidth': video.videoWidth, | ||
| 145 | + 'videoHeight': video.videoHeight, | ||
| 146 | + 'torchable': hasFlash | ||
| 147 | + }; | ||
| 148 | + } catch (e) { | ||
| 149 | + throw PlatformException(code: 'MobileScannerWeb', message: e.toString()); | ||
| 150 | + } | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | + /// Check if any camera's are available | ||
| 154 | + static Future<bool> cameraAvailable() async { | ||
| 155 | + final sources = | ||
| 156 | + await html.window.navigator.mediaDevices!.enumerateDevices(); | ||
| 157 | + for (final e in sources) { | ||
| 158 | + if (e.kind == 'videoinput') { | ||
| 159 | + return true; | ||
| 160 | + } | ||
| 161 | + } | ||
| 162 | + return false; | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + /// Stops the video feed and analyzer | ||
| 166 | + Future<void> cancel() async { | ||
| 167 | + try { | ||
| 168 | + // Stop the camera stream | ||
| 169 | + _localStream!.getTracks().forEach((track) { | ||
| 170 | + if (track.readyState == 'live') { | ||
| 171 | + track.stop(); | ||
| 172 | + } | ||
| 173 | + }); | ||
| 174 | + } catch (e) { | ||
| 175 | + debugPrint('Failed to stop stream: $e'); | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + video.srcObject = null; | ||
| 179 | + _localStream = null; | ||
| 180 | + _frameInterval?.cancel(); | ||
| 181 | + _frameInterval = null; | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + /// Captures a frame and analyzes it for QR codes | ||
| 185 | + Future<dynamic> _captureFrame() async { | ||
| 186 | + if (_localStream == null) return null; | ||
| 187 | + final canvas = | ||
| 188 | + html.CanvasElement(width: video.videoWidth, height: video.videoHeight); | ||
| 189 | + final ctx = canvas.context2D; | ||
| 190 | + | ||
| 191 | + ctx.drawImage(video, 0, 0); | ||
| 192 | + final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!); | ||
| 193 | + | ||
| 194 | + final code = jsQR(imgData.data, canvas.width, canvas.height); | ||
| 195 | + if (code != null) { | ||
| 196 | + controller.add({'name': 'barcodeWeb', 'data': code.data}); | ||
| 197 | + } | ||
| 198 | + } | ||
| 199 | +} |
| @@ -2,10 +2,6 @@ import 'package:flutter/foundation.dart'; | @@ -2,10 +2,6 @@ import 'package:flutter/foundation.dart'; | ||
| 2 | import 'package:flutter/material.dart'; | 2 | import 'package:flutter/material.dart'; |
| 3 | import 'package:mobile_scanner/mobile_scanner.dart'; | 3 | import 'package:mobile_scanner/mobile_scanner.dart'; |
| 4 | 4 | ||
| 5 | -import 'mobile_scanner_arguments.dart'; | ||
| 6 | - | ||
| 7 | -import 'web/flutter_qr_web.dart'; | ||
| 8 | - | ||
| 9 | enum Ratio { ratio_4_3, ratio_16_9 } | 5 | enum Ratio { ratio_4_3, ratio_16_9 } |
| 10 | 6 | ||
| 11 | /// A widget showing a live camera preview. | 7 | /// A widget showing a live camera preview. |
| @@ -64,12 +60,6 @@ class _MobileScannerState extends State<MobileScanner> | @@ -64,12 +60,6 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 64 | 60 | ||
| 65 | @override | 61 | @override |
| 66 | Widget build(BuildContext context) { | 62 | Widget build(BuildContext context) { |
| 67 | - if (kIsWeb) { | ||
| 68 | - return WebScanner( | ||
| 69 | - onDetect: (barcode) => widget.onDetect!(barcode, null), | ||
| 70 | - cameraFacing: CameraFacing.back, | ||
| 71 | - ); | ||
| 72 | - } else { | ||
| 73 | return LayoutBuilder(builder: (context, BoxConstraints constraints) { | 63 | return LayoutBuilder(builder: (context, BoxConstraints constraints) { |
| 74 | return ValueListenableBuilder( | 64 | return ValueListenableBuilder( |
| 75 | valueListenable: controller.args, | 65 | valueListenable: controller.args, |
| @@ -96,7 +86,7 @@ class _MobileScannerState extends State<MobileScanner> | @@ -96,7 +86,7 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 96 | child: SizedBox( | 86 | child: SizedBox( |
| 97 | width: value.size.width, | 87 | width: value.size.width, |
| 98 | height: value.size.height, | 88 | height: value.size.height, |
| 99 | - child: Texture(textureId: value.textureId), | 89 | + child: kIsWeb ? HtmlElementView(viewType: value.webId!) : Texture(textureId: value.textureId!), |
| 100 | ), | 90 | ), |
| 101 | ), | 91 | ), |
| 102 | ), | 92 | ), |
| @@ -105,7 +95,6 @@ class _MobileScannerState extends State<MobileScanner> | @@ -105,7 +95,6 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 105 | }); | 95 | }); |
| 106 | }); | 96 | }); |
| 107 | } | 97 | } |
| 108 | - } | ||
| 109 | 98 | ||
| 110 | @override | 99 | @override |
| 111 | void didUpdateWidget(covariant MobileScanner oldWidget) { | 100 | void didUpdateWidget(covariant MobileScanner oldWidget) { |
| @@ -3,14 +3,16 @@ import 'package:flutter/material.dart'; | @@ -3,14 +3,16 @@ import 'package:flutter/material.dart'; | ||
| 3 | /// Camera args for [CameraView]. | 3 | /// Camera args for [CameraView]. |
| 4 | class MobileScannerArguments { | 4 | class MobileScannerArguments { |
| 5 | /// The texture id. | 5 | /// The texture id. |
| 6 | - final int textureId; | 6 | + final int? textureId; |
| 7 | 7 | ||
| 8 | /// Size of the texture. | 8 | /// Size of the texture. |
| 9 | final Size size; | 9 | final Size size; |
| 10 | 10 | ||
| 11 | final bool hasTorch; | 11 | final bool hasTorch; |
| 12 | 12 | ||
| 13 | + final String? webId; | ||
| 14 | + | ||
| 13 | /// Create a [MobileScannerArguments]. | 15 | /// Create a [MobileScannerArguments]. |
| 14 | MobileScannerArguments( | 16 | MobileScannerArguments( |
| 15 | - {required this.textureId, required this.size, required this.hasTorch}); | 17 | + {this.textureId, required this.size, required this.hasTorch, this.webId}); |
| 16 | } | 18 | } |
| @@ -2,6 +2,7 @@ import 'dart:async'; | @@ -2,6 +2,7 @@ import 'dart:async'; | ||
| 2 | import 'dart:io'; | 2 | import 'dart:io'; |
| 3 | 3 | ||
| 4 | import 'package:flutter/cupertino.dart'; | 4 | import 'package:flutter/cupertino.dart'; |
| 5 | +import 'package:flutter/foundation.dart'; | ||
| 5 | import 'package:flutter/services.dart'; | 6 | import 'package:flutter/services.dart'; |
| 6 | import 'package:mobile_scanner/mobile_scanner.dart'; | 7 | import 'package:mobile_scanner/mobile_scanner.dart'; |
| 7 | 8 | ||
| @@ -98,6 +99,9 @@ class MobileScannerController { | @@ -98,6 +99,9 @@ class MobileScannerController { | ||
| 98 | case 'barcodeMac': | 99 | case 'barcodeMac': |
| 99 | barcodesController.add(Barcode(rawValue: data['payload'])); | 100 | barcodesController.add(Barcode(rawValue: data['payload'])); |
| 100 | break; | 101 | break; |
| 102 | + case 'barcodeWeb': | ||
| 103 | + barcodesController.add(Barcode(rawValue: data)); | ||
| 104 | + break; | ||
| 101 | default: | 105 | default: |
| 102 | throw UnimplementedError(); | 106 | throw UnimplementedError(); |
| 103 | } | 107 | } |
| @@ -125,19 +129,22 @@ class MobileScannerController { | @@ -125,19 +129,22 @@ class MobileScannerController { | ||
| 125 | // setAnalyzeMode(AnalyzeMode.barcode.index); | 129 | // setAnalyzeMode(AnalyzeMode.barcode.index); |
| 126 | 130 | ||
| 127 | // Check authorization status | 131 | // Check authorization status |
| 128 | - MobileScannerState state = | ||
| 129 | - MobileScannerState.values[await methodChannel.invokeMethod('state')]; | ||
| 130 | - switch (state) { | ||
| 131 | - case MobileScannerState.undetermined: | ||
| 132 | - final bool result = await methodChannel.invokeMethod('request'); | ||
| 133 | - state = | ||
| 134 | - result ? MobileScannerState.authorized : MobileScannerState.denied; | ||
| 135 | - break; | ||
| 136 | - case MobileScannerState.denied: | ||
| 137 | - isStarting = false; | ||
| 138 | - throw PlatformException(code: 'NO ACCESS'); | ||
| 139 | - case MobileScannerState.authorized: | ||
| 140 | - break; | 132 | + if (!kIsWeb) { |
| 133 | + MobileScannerState state = | ||
| 134 | + MobileScannerState.values[await methodChannel.invokeMethod('state')]; | ||
| 135 | + switch (state) { | ||
| 136 | + case MobileScannerState.undetermined: | ||
| 137 | + final bool result = await methodChannel.invokeMethod('request'); | ||
| 138 | + state = result | ||
| 139 | + ? MobileScannerState.authorized | ||
| 140 | + : MobileScannerState.denied; | ||
| 141 | + break; | ||
| 142 | + case MobileScannerState.denied: | ||
| 143 | + isStarting = false; | ||
| 144 | + throw PlatformException(code: 'NO ACCESS'); | ||
| 145 | + case MobileScannerState.authorized: | ||
| 146 | + break; | ||
| 147 | + } | ||
| 141 | } | 148 | } |
| 142 | 149 | ||
| 143 | cameraFacingState.value = facing; | 150 | cameraFacingState.value = facing; |
| @@ -174,10 +181,20 @@ class MobileScannerController { | @@ -174,10 +181,20 @@ class MobileScannerController { | ||
| 174 | } | 181 | } |
| 175 | 182 | ||
| 176 | hasTorch = startResult['torchable']; | 183 | hasTorch = startResult['torchable']; |
| 177 | - args.value = MobileScannerArguments( | ||
| 178 | - textureId: startResult['textureId'], | ||
| 179 | - size: toSize(startResult['size']), | ||
| 180 | - hasTorch: hasTorch); | 184 | + |
| 185 | + if (kIsWeb) { | ||
| 186 | + args.value = MobileScannerArguments( | ||
| 187 | + webId: startResult['ViewID'], | ||
| 188 | + size: Size(startResult['videoWidth'], startResult['videoHeight']), | ||
| 189 | + hasTorch: hasTorch); | ||
| 190 | + } else { | ||
| 191 | + | ||
| 192 | + args.value = MobileScannerArguments( | ||
| 193 | + textureId: startResult['textureId'], | ||
| 194 | + size: toSize(startResult['size']), | ||
| 195 | + hasTorch: hasTorch); | ||
| 196 | + } | ||
| 197 | + | ||
| 181 | isStarting = false; | 198 | isStarting = false; |
| 182 | } | 199 | } |
| 183 | 200 |
lib/src/web/flutter_qr_web.dart
deleted
100644 → 0
| 1 | -// ignore_for_file: avoid_web_libraries_in_flutter | ||
| 2 | - | ||
| 3 | -import 'dart:async'; | ||
| 4 | -import 'dart:core'; | ||
| 5 | -import 'dart:html' as html; | ||
| 6 | -import 'dart:ui' as ui; | ||
| 7 | - | ||
| 8 | -import 'package:flutter/material.dart'; | ||
| 9 | - | ||
| 10 | -import '../../mobile_scanner.dart'; | ||
| 11 | -import 'jsqr.dart'; | ||
| 12 | -import 'media.dart'; | ||
| 13 | - | ||
| 14 | -/// Even though it has been highly modified, the origial implementation has been | ||
| 15 | -/// adopted from https://github.com:treeder/jsqr_flutter | ||
| 16 | -/// | ||
| 17 | -/// Copyright 2020 @treeder | ||
| 18 | -/// Copyright 2021 The one with the braid | ||
| 19 | - | ||
| 20 | -class WebScanner extends StatefulWidget { | ||
| 21 | - final Function(Barcode) onDetect; | ||
| 22 | - final CameraFacing? cameraFacing; | ||
| 23 | - | ||
| 24 | - const WebScanner( | ||
| 25 | - {Key? key, | ||
| 26 | - required this.onDetect, | ||
| 27 | - this.cameraFacing = CameraFacing.front}) | ||
| 28 | - : super(key: key); | ||
| 29 | - | ||
| 30 | - @override | ||
| 31 | - _WebScannerState createState() => _WebScannerState(); | ||
| 32 | - | ||
| 33 | - // need a global for the registerViewFactory | ||
| 34 | - static html.DivElement vidDiv = html.DivElement(); | ||
| 35 | - | ||
| 36 | - static Future<bool> cameraAvailable() async { | ||
| 37 | - final sources = | ||
| 38 | - await html.window.navigator.mediaDevices!.enumerateDevices(); | ||
| 39 | - // List<String> vidIds = []; | ||
| 40 | - var hasCam = false; | ||
| 41 | - for (final e in sources) { | ||
| 42 | - if (e.kind == 'videoinput') { | ||
| 43 | - // vidIds.add(e['deviceId']); | ||
| 44 | - hasCam = true; | ||
| 45 | - } | ||
| 46 | - } | ||
| 47 | - return hasCam; | ||
| 48 | - } | ||
| 49 | -} | ||
| 50 | - | ||
| 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 | ||
| 56 | - html.MediaStream? _localStream; | ||
| 57 | - | ||
| 58 | - // Check if analyzer is processing barcode | ||
| 59 | - bool _currentlyProcessing = false; | ||
| 60 | - | ||
| 61 | - // QRViewControllerWeb? _controller; | ||
| 62 | - | ||
| 63 | - // Set size of the webview | ||
| 64 | - // Size _size = const Size(0, 0); | ||
| 65 | - | ||
| 66 | - // TODO: Timer for capture? | ||
| 67 | - Timer? timer; | ||
| 68 | - | ||
| 69 | - // String? code; | ||
| 70 | - | ||
| 71 | - // TODO: Error message if error | ||
| 72 | - String? _errorMsg; | ||
| 73 | - | ||
| 74 | - // Video element to be played on | ||
| 75 | - html.VideoElement video = html.VideoElement(); | ||
| 76 | - | ||
| 77 | - // ID of the video feed | ||
| 78 | - String viewID = | ||
| 79 | - 'WebScanner-' + DateTime.now().millisecondsSinceEpoch.toString(); | ||
| 80 | - | ||
| 81 | - // final StreamController<Barcode> _scanUpdateController = | ||
| 82 | - // StreamController<Barcode>(); | ||
| 83 | - | ||
| 84 | - // Timer for interval capture | ||
| 85 | - Timer? _frameIntervall; | ||
| 86 | - | ||
| 87 | - @override | ||
| 88 | - void initState() { | ||
| 89 | - super.initState(); | ||
| 90 | - // facing = widget.cameraFacing ?? CameraFacing.front; | ||
| 91 | - WebScanner.vidDiv.children = [video]; | ||
| 92 | - | ||
| 93 | - // ignore: UNDEFINED_PREFIXED_NAME | ||
| 94 | - ui.platformViewRegistry | ||
| 95 | - .registerViewFactory(viewID, (int id) => WebScanner.vidDiv); | ||
| 96 | - | ||
| 97 | - // giving JavaScipt some time to process the DOM changes | ||
| 98 | - Timer(const Duration(milliseconds: 500), () { | ||
| 99 | - start(); | ||
| 100 | - }); | ||
| 101 | - } | ||
| 102 | - | ||
| 103 | - /// Initialize camera and capture frame | ||
| 104 | - Future start() async { | ||
| 105 | - await _startVideoStream(); | ||
| 106 | - _frameIntervall?.cancel(); | ||
| 107 | - _frameIntervall = | ||
| 108 | - Timer.periodic(const Duration(milliseconds: 200), (timer) { | ||
| 109 | - _captureFrame(); | ||
| 110 | - }); | ||
| 111 | - } | ||
| 112 | - | ||
| 113 | - void cancel() { | ||
| 114 | - if (timer != null) { | ||
| 115 | - timer!.cancel(); | ||
| 116 | - timer = null; | ||
| 117 | - } | ||
| 118 | - if (_currentlyProcessing) { | ||
| 119 | - _stopVideoStream(); | ||
| 120 | - } | ||
| 121 | - } | ||
| 122 | - | ||
| 123 | - @override | ||
| 124 | - void dispose() { | ||
| 125 | - cancel(); | ||
| 126 | - super.dispose(); | ||
| 127 | - } | ||
| 128 | - | ||
| 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; | ||
| 133 | - | ||
| 134 | - try { | ||
| 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( | ||
| 140 | - video: VideoOptions( | ||
| 141 | - facingMode: (widget.cameraFacing == CameraFacing.front | ||
| 142 | - ? 'user' | ||
| 143 | - : 'environment'), | ||
| 144 | - width: {'ideal': 4096}, | ||
| 145 | - height: {'ideal': 2160}, | ||
| 146 | - )); | ||
| 147 | - | ||
| 148 | - _localStream = | ||
| 149 | - await html.window.navigator.getUserMedia(video: constraints); | ||
| 150 | - } else { | ||
| 151 | - _localStream = await html.window.navigator.getUserMedia(video: true); | ||
| 152 | - } | ||
| 153 | - | ||
| 154 | - video.srcObject = _localStream; | ||
| 155 | - | ||
| 156 | - // required to tell iOS safari we don't want fullscreen | ||
| 157 | - video.setAttribute('playsinline', 'true'); | ||
| 158 | - | ||
| 159 | - // TODO: Check controller | ||
| 160 | - // if (_controller == null) { | ||
| 161 | - // _controller = QRViewControllerWeb(this); | ||
| 162 | - // widget.onPlatformViewCreated(_controller!); | ||
| 163 | - // } | ||
| 164 | - | ||
| 165 | - await video.play(); | ||
| 166 | - } catch (e) { | ||
| 167 | - cancel(); | ||
| 168 | - setState(() { | ||
| 169 | - _errorMsg = e.toString(); | ||
| 170 | - }); | ||
| 171 | - return; | ||
| 172 | - } | ||
| 173 | - | ||
| 174 | - if (!mounted) return; | ||
| 175 | - | ||
| 176 | - setState(() { | ||
| 177 | - _currentlyProcessing = true; | ||
| 178 | - }); | ||
| 179 | - } | ||
| 180 | - | ||
| 181 | - Future<void> _stopVideoStream() async { | ||
| 182 | - try { | ||
| 183 | - // Stop the camera stream | ||
| 184 | - _localStream!.getTracks().forEach((track) { | ||
| 185 | - if (track.readyState == 'live') { | ||
| 186 | - track.stop(); | ||
| 187 | - } | ||
| 188 | - }); | ||
| 189 | - | ||
| 190 | - video.srcObject = null; | ||
| 191 | - _localStream = null; | ||
| 192 | - } catch (e) { | ||
| 193 | - debugPrint('Failed to stop stream: $e'); | ||
| 194 | - } | ||
| 195 | - } | ||
| 196 | - | ||
| 197 | - Future<dynamic> _captureFrame() async { | ||
| 198 | - if (_localStream == null) return null; | ||
| 199 | - final canvas = html.CanvasElement(width: video.videoWidth, height: video.videoHeight); | ||
| 200 | - final ctx = canvas.context2D; | ||
| 201 | - | ||
| 202 | - ctx.drawImage(video, 0, 0); | ||
| 203 | - final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!); | ||
| 204 | - | ||
| 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}'); | ||
| 213 | - final code = jsQR(imgData.data, canvas.width, canvas.height); | ||
| 214 | - // ignore: unnecessary_null_comparison | ||
| 215 | - if (code != null) { | ||
| 216 | - debugPrint('CODE: $code'); | ||
| 217 | - // widget.onDetect(Barcode(rawValue: code.data)); | ||
| 218 | - // print('Barcode: ${code.data}'); | ||
| 219 | - // _scanUpdateController | ||
| 220 | - // .add(Barcode(rawValue: code.data)); | ||
| 221 | - } | ||
| 222 | - } | ||
| 223 | - | ||
| 224 | - @override | ||
| 225 | - Widget build(BuildContext context) { | ||
| 226 | - if (_errorMsg != null) { | ||
| 227 | - return Center(child: Text(_errorMsg!)); | ||
| 228 | - } | ||
| 229 | - if (_localStream == null) { | ||
| 230 | - return const Center(child: CircularProgressIndicator()); | ||
| 231 | - } | ||
| 232 | - | ||
| 233 | - return SizedBox( | ||
| 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)))); | ||
| 241 | - } | ||
| 242 | -} |
| @@ -4,7 +4,7 @@ library jsqr; | @@ -4,7 +4,7 @@ library jsqr; | ||
| 4 | import 'package:js/js.dart'; | 4 | import 'package:js/js.dart'; |
| 5 | 5 | ||
| 6 | @JS('jsQR') | 6 | @JS('jsQR') |
| 7 | -external Code jsQR(var data, int? width, int? height); | 7 | +external Code? jsQR(var data, int? width, int? height); |
| 8 | 8 | ||
| 9 | @JS() | 9 | @JS() |
| 10 | class Code { | 10 | class Code { |
| @@ -11,6 +11,8 @@ dependencies: | @@ -11,6 +11,8 @@ dependencies: | ||
| 11 | js: ^0.6.4 | 11 | js: ^0.6.4 |
| 12 | flutter: | 12 | flutter: |
| 13 | sdk: flutter | 13 | sdk: flutter |
| 14 | + flutter_web_plugins: | ||
| 15 | + sdk: flutter | ||
| 14 | 16 | ||
| 15 | dev_dependencies: | 17 | dev_dependencies: |
| 16 | flutter_test: | 18 | flutter_test: |
| @@ -26,4 +28,7 @@ flutter: | @@ -26,4 +28,7 @@ flutter: | ||
| 26 | ios: | 28 | ios: |
| 27 | pluginClass: MobileScannerPlugin | 29 | pluginClass: MobileScannerPlugin |
| 28 | macos: | 30 | macos: |
| 29 | - pluginClass: MobileScannerPlugin | ||
| 31 | + pluginClass: MobileScannerPlugin | ||
| 32 | + web: | ||
| 33 | + pluginClass: MobileScannerWebPlugin | ||
| 34 | + fileName: mobile_scanner_web_plugin.dart |
-
Please register or login to post a comment