Showing
13 changed files
with
488 additions
and
45 deletions
example/web/favicon.png
0 → 100644
917 Bytes
example/web/icons/Icon-192.png
0 → 100644
5.17 KB
example/web/icons/Icon-512.png
0 → 100644
8.06 KB
example/web/index.html
0 → 100644
| 1 | +<!DOCTYPE html> | ||
| 2 | +<html> | ||
| 3 | +<head> | ||
| 4 | + <!-- | ||
| 5 | + If you are serving your web app in a path other than the root, change the | ||
| 6 | + href value below to reflect the base path you are serving from. | ||
| 7 | + | ||
| 8 | + The path provided below has to start and end with a slash "/" in order for | ||
| 9 | + it to work correctly. | ||
| 10 | + | ||
| 11 | + Fore more details: | ||
| 12 | + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base | ||
| 13 | + --> | ||
| 14 | + <base href="/"> | ||
| 15 | + | ||
| 16 | + <meta charset="UTF-8"> | ||
| 17 | + <meta content="IE=Edge" http-equiv="X-UA-Compatible"> | ||
| 18 | + <meta name="description" content="A new Flutter project."> | ||
| 19 | + | ||
| 20 | + <!-- iOS meta tags & icons --> | ||
| 21 | + <meta name="apple-mobile-web-app-capable" content="yes"> | ||
| 22 | + <meta name="apple-mobile-web-app-status-bar-style" content="black"> | ||
| 23 | + <meta name="apple-mobile-web-app-title" content="example"> | ||
| 24 | + <link rel="apple-touch-icon" href="icons/Icon-192.png"> | ||
| 25 | + | ||
| 26 | + <!-- Favicon --> | ||
| 27 | + <link rel="icon" type="image/png" href="favicon.png"/> | ||
| 28 | + | ||
| 29 | + <title>example</title> | ||
| 30 | + <link rel="manifest" href="manifest.json"> | ||
| 31 | + <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script> | ||
| 32 | +</head> | ||
| 33 | +<body> | ||
| 34 | + <!-- This script installs service_worker.js to provide PWA functionality to | ||
| 35 | + application. For more information, see: | ||
| 36 | + https://developers.google.com/web/fundamentals/primers/service-workers --> | ||
| 37 | + <script> | ||
| 38 | + if ('serviceWorker' in navigator) { | ||
| 39 | + window.addEventListener('flutter-first-frame', function () { | ||
| 40 | + navigator.serviceWorker.register('flutter_service_worker.js'); | ||
| 41 | + }); | ||
| 42 | + } | ||
| 43 | + </script> | ||
| 44 | + <script src="main.dart.js" type="application/javascript"></script> | ||
| 45 | +</body> | ||
| 46 | +</html> |
example/web/manifest.json
0 → 100644
| 1 | +{ | ||
| 2 | + "name": "example", | ||
| 3 | + "short_name": "example", | ||
| 4 | + "start_url": ".", | ||
| 5 | + "display": "standalone", | ||
| 6 | + "background_color": "#0175C2", | ||
| 7 | + "theme_color": "#0175C2", | ||
| 8 | + "description": "A new Flutter project.", | ||
| 9 | + "orientation": "portrait-primary", | ||
| 10 | + "prefer_related_applications": false, | ||
| 11 | + "icons": [ | ||
| 12 | + { | ||
| 13 | + "src": "icons/Icon-192.png", | ||
| 14 | + "sizes": "192x192", | ||
| 15 | + "type": "image/png" | ||
| 16 | + }, | ||
| 17 | + { | ||
| 18 | + "src": "icons/Icon-512.png", | ||
| 19 | + "sizes": "512x512", | ||
| 20 | + "type": "image/png" | ||
| 21 | + } | ||
| 22 | + ] | ||
| 23 | +} |
| 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 | ||
| 4 | import 'mobile_scanner_arguments.dart'; | 5 | import 'mobile_scanner_arguments.dart'; |
| 5 | 6 | ||
| 7 | +import 'web/flutter_qr_web.dart'; | ||
| 8 | + | ||
| 6 | enum Ratio { ratio_4_3, ratio_16_9 } | 9 | enum Ratio { ratio_4_3, ratio_16_9 } |
| 7 | 10 | ||
| 8 | /// A widget showing a live camera preview. | 11 | /// A widget showing a live camera preview. |
| @@ -14,7 +17,7 @@ class MobileScanner extends StatefulWidget { | @@ -14,7 +17,7 @@ class MobileScanner extends StatefulWidget { | ||
| 14 | /// | 17 | /// |
| 15 | /// [barcode] The barcode object with all information about the scanned code. | 18 | /// [barcode] The barcode object with all information about the scanned code. |
| 16 | /// [args] Information about the state of the MobileScanner widget | 19 | /// [args] Information about the state of the MobileScanner widget |
| 17 | - final Function(Barcode barcode, MobileScannerArguments args)? onDetect; | 20 | + final Function(Barcode barcode, MobileScannerArguments? args)? onDetect; |
| 18 | 21 | ||
| 19 | /// TODO: Function that gets called when the Widget is initialized. Can be usefull | 22 | /// TODO: Function that gets called when the Widget is initialized. Can be usefull |
| 20 | /// to check wether the device has a torch(flash) or not. | 23 | /// to check wether the device has a torch(flash) or not. |
| @@ -66,34 +69,48 @@ class _MobileScannerState extends State<MobileScanner> | @@ -66,34 +69,48 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 66 | 69 | ||
| 67 | @override | 70 | @override |
| 68 | Widget build(BuildContext context) { | 71 | Widget build(BuildContext context) { |
| 69 | - return LayoutBuilder(builder: (context, BoxConstraints constraints) { | ||
| 70 | - if (!onScreen) return const Text("Camera Paused."); | ||
| 71 | - return ValueListenableBuilder( | ||
| 72 | - valueListenable: controller.args, | ||
| 73 | - builder: (context, value, child) { | ||
| 74 | - value = value as MobileScannerArguments?; | ||
| 75 | - if (value == null) { | ||
| 76 | - return Container(color: Colors.black); | ||
| 77 | - } else { | ||
| 78 | - controller.barcodes.listen( | ||
| 79 | - (a) => widget.onDetect!(a, value as MobileScannerArguments)); | ||
| 80 | - return ClipRect( | ||
| 81 | - child: SizedBox( | ||
| 82 | - width: MediaQuery.of(context).size.width, | ||
| 83 | - height: MediaQuery.of(context).size.height, | ||
| 84 | - child: FittedBox( | ||
| 85 | - fit: widget.fit, | ||
| 86 | - child: SizedBox( | ||
| 87 | - width: value.size.width, | ||
| 88 | - height: value.size.height, | ||
| 89 | - child: Texture(textureId: value.textureId), | 72 | + if (kIsWeb) { |
| 73 | + return createWebQrView( | ||
| 74 | + onDetect: (barcode) => widget.onDetect!(barcode, null), | ||
| 75 | + cameraFacing: CameraFacing.back, | ||
| 76 | + ); | ||
| 77 | + } else { | ||
| 78 | + return LayoutBuilder(builder: (context, BoxConstraints constraints) { | ||
| 79 | + if (!onScreen) return const Text("Camera Paused."); | ||
| 80 | + return ValueListenableBuilder( | ||
| 81 | + valueListenable: controller.args, | ||
| 82 | + builder: (context, value, child) { | ||
| 83 | + value = value as MobileScannerArguments?; | ||
| 84 | + if (value == null) { | ||
| 85 | + return Container(color: Colors.black); | ||
| 86 | + } else { | ||
| 87 | + controller.barcodes.listen( | ||
| 88 | + (a) => | ||
| 89 | + widget.onDetect!(a, value as MobileScannerArguments)); | ||
| 90 | + return ClipRect( | ||
| 91 | + child: SizedBox( | ||
| 92 | + width: MediaQuery | ||
| 93 | + .of(context) | ||
| 94 | + .size | ||
| 95 | + .width, | ||
| 96 | + height: MediaQuery | ||
| 97 | + .of(context) | ||
| 98 | + .size | ||
| 99 | + .height, | ||
| 100 | + child: FittedBox( | ||
| 101 | + fit: widget.fit, | ||
| 102 | + child: SizedBox( | ||
| 103 | + width: value.size.width, | ||
| 104 | + height: value.size.height, | ||
| 105 | + child: Texture(textureId: value.textureId), | ||
| 106 | + ), | ||
| 90 | ), | 107 | ), |
| 91 | ), | 108 | ), |
| 92 | - ), | ||
| 93 | - ); | ||
| 94 | - } | ||
| 95 | - }); | ||
| 96 | - }); | 109 | + ); |
| 110 | + } | ||
| 111 | + }); | ||
| 112 | + }); | ||
| 113 | + } | ||
| 97 | } | 114 | } |
| 98 | 115 | ||
| 99 | @override | 116 | @override |
lib/src/web/flutter_qr_stub.dart
0 → 100644
lib/src/web/flutter_qr_web.dart
0 → 100644
| 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:js_util'; | ||
| 7 | +import 'dart:ui' as ui; | ||
| 8 | + | ||
| 9 | +import 'package:flutter/material.dart'; | ||
| 10 | + | ||
| 11 | +import '../../mobile_scanner.dart'; | ||
| 12 | +import 'jsqr.dart'; | ||
| 13 | +import 'media.dart'; | ||
| 14 | + | ||
| 15 | +/// Even though it has been highly modified, the origial implementation has been | ||
| 16 | +/// adopted from https://github.com:treeder/jsqr_flutter | ||
| 17 | +/// | ||
| 18 | +/// Copyright 2020 @treeder | ||
| 19 | +/// Copyright 2021 The one with the braid | ||
| 20 | + | ||
| 21 | +class WebQrView extends StatefulWidget { | ||
| 22 | + final Function(Barcode) onDetect; | ||
| 23 | + final CameraFacing? cameraFacing; | ||
| 24 | + | ||
| 25 | + const WebQrView( | ||
| 26 | + {Key? key, | ||
| 27 | + required this.onDetect, | ||
| 28 | + this.cameraFacing = CameraFacing.front}) | ||
| 29 | + : super(key: key); | ||
| 30 | + | ||
| 31 | + @override | ||
| 32 | + _WebQrViewState createState() => _WebQrViewState(); | ||
| 33 | + | ||
| 34 | + static html.DivElement vidDiv = | ||
| 35 | + html.DivElement(); // need a global for the registerViewFactory | ||
| 36 | + | ||
| 37 | + static Future<bool> cameraAvailable() async { | ||
| 38 | + final sources = | ||
| 39 | + await html.window.navigator.mediaDevices!.enumerateDevices(); | ||
| 40 | + // List<String> vidIds = []; | ||
| 41 | + var hasCam = false; | ||
| 42 | + for (final e in sources) { | ||
| 43 | + if (e.kind == 'videoinput') { | ||
| 44 | + // vidIds.add(e['deviceId']); | ||
| 45 | + hasCam = true; | ||
| 46 | + } | ||
| 47 | + } | ||
| 48 | + return hasCam; | ||
| 49 | + } | ||
| 50 | +} | ||
| 51 | + | ||
| 52 | +class _WebQrViewState extends State<WebQrView> { | ||
| 53 | + html.MediaStream? _localStream; | ||
| 54 | + // html.CanvasElement canvas; | ||
| 55 | + // html.CanvasRenderingContext2D ctx; | ||
| 56 | + bool _currentlyProcessing = false; | ||
| 57 | + | ||
| 58 | + // QRViewControllerWeb? _controller; | ||
| 59 | + | ||
| 60 | + late Size _size = const Size(0, 0); | ||
| 61 | + Timer? timer; | ||
| 62 | + String? code; | ||
| 63 | + String? _errorMsg; | ||
| 64 | + html.VideoElement video = html.VideoElement(); | ||
| 65 | + String viewID = 'QRVIEW-' + DateTime.now().millisecondsSinceEpoch.toString(); | ||
| 66 | + | ||
| 67 | + final StreamController<Barcode> _scanUpdateController = | ||
| 68 | + StreamController<Barcode>(); | ||
| 69 | + late CameraFacing facing; | ||
| 70 | + | ||
| 71 | + Timer? _frameIntervall; | ||
| 72 | + | ||
| 73 | + @override | ||
| 74 | + void initState() { | ||
| 75 | + super.initState(); | ||
| 76 | + | ||
| 77 | + facing = widget.cameraFacing ?? CameraFacing.front; | ||
| 78 | + | ||
| 79 | + // video = html.VideoElement(); | ||
| 80 | + WebQrView.vidDiv.children = [video]; | ||
| 81 | + // ignore: UNDEFINED_PREFIXED_NAME | ||
| 82 | + ui.platformViewRegistry | ||
| 83 | + .registerViewFactory(viewID, (int id) => WebQrView.vidDiv); | ||
| 84 | + // giving JavaScipt some time to process the DOM changes | ||
| 85 | + Timer(const Duration(milliseconds: 500), () { | ||
| 86 | + start(); | ||
| 87 | + }); | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + Future start() async { | ||
| 91 | + await _makeCall(); | ||
| 92 | + _frameIntervall?.cancel(); | ||
| 93 | + _frameIntervall = | ||
| 94 | + Timer.periodic(const Duration(milliseconds: 200), (timer) { | ||
| 95 | + _captureFrame2(); | ||
| 96 | + }); | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + void cancel() { | ||
| 100 | + if (timer != null) { | ||
| 101 | + timer!.cancel(); | ||
| 102 | + timer = null; | ||
| 103 | + } | ||
| 104 | + if (_currentlyProcessing) { | ||
| 105 | + _stopStream(); | ||
| 106 | + } | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + @override | ||
| 110 | + void dispose() { | ||
| 111 | + cancel(); | ||
| 112 | + super.dispose(); | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + // Platform messages are asynchronous, so we initialize in an async method. | ||
| 116 | + Future<void> _makeCall() async { | ||
| 117 | + if (_localStream != null) { | ||
| 118 | + return; | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + try { | ||
| 122 | + var constraints = UserMediaOptions( | ||
| 123 | + video: VideoOptions( | ||
| 124 | + facingMode: (facing == CameraFacing.front ? 'user' : 'environment'), | ||
| 125 | + )); | ||
| 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; | ||
| 132 | + video.srcObject = _localStream; | ||
| 133 | + video.setAttribute('playsinline', | ||
| 134 | + 'true'); // required to tell iOS safari we don't want fullscreen | ||
| 135 | + // if (_controller == null) { | ||
| 136 | + // _controller = QRViewControllerWeb(this); | ||
| 137 | + // widget.onPlatformViewCreated(_controller!); | ||
| 138 | + // } | ||
| 139 | + await video.play(); | ||
| 140 | + } catch (e) { | ||
| 141 | + cancel(); | ||
| 142 | + setState(() { | ||
| 143 | + _errorMsg = e.toString(); | ||
| 144 | + }); | ||
| 145 | + return; | ||
| 146 | + } | ||
| 147 | + if (!mounted) return; | ||
| 148 | + | ||
| 149 | + setState(() { | ||
| 150 | + _currentlyProcessing = true; | ||
| 151 | + }); | ||
| 152 | + } | ||
| 153 | + | ||
| 154 | + Future<void> _stopStream() async { | ||
| 155 | + try { | ||
| 156 | + // await _localStream.dispose(); | ||
| 157 | + _localStream!.getTracks().forEach((track) { | ||
| 158 | + if (track.readyState == 'live') { | ||
| 159 | + track.stop(); | ||
| 160 | + } | ||
| 161 | + }); | ||
| 162 | + // video.stop(); | ||
| 163 | + video.srcObject = null; | ||
| 164 | + _localStream = null; | ||
| 165 | + // _localRenderer.srcObject = null; | ||
| 166 | + // ignore: empty_catches | ||
| 167 | + } catch (e) {} | ||
| 168 | + } | ||
| 169 | + | ||
| 170 | + Future<dynamic> _captureFrame2() async { | ||
| 171 | + if (_localStream == null) { | ||
| 172 | + return null; | ||
| 173 | + } | ||
| 174 | + final canvas = | ||
| 175 | + html.CanvasElement(width: video.videoWidth, height: video.videoHeight); | ||
| 176 | + final ctx = canvas.context2D; | ||
| 177 | + // canvas.width = video.videoWidth; | ||
| 178 | + // canvas.height = video.videoHeight; | ||
| 179 | + ctx.drawImage(video, 0, 0); | ||
| 180 | + final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!); | ||
| 181 | + | ||
| 182 | + final size = | ||
| 183 | + Size(canvas.width?.toDouble() ?? 0, canvas.height?.toDouble() ?? 0); | ||
| 184 | + if (size != _size) { | ||
| 185 | + setState(() { | ||
| 186 | + _setCanvasSize(size); | ||
| 187 | + }); | ||
| 188 | + } | ||
| 189 | + final code = jsQR(imgData.data, canvas.width, canvas.height); | ||
| 190 | + // ignore: unnecessary_null_comparison | ||
| 191 | + if (code != null) { | ||
| 192 | + widget.onDetect(Barcode(rawValue: code.data)); | ||
| 193 | + // print('Barcode: ${code.data}'); | ||
| 194 | + // _scanUpdateController | ||
| 195 | + // .add(Barcode(rawValue: code.data)); | ||
| 196 | + } | ||
| 197 | + } | ||
| 198 | + | ||
| 199 | + @override | ||
| 200 | + Widget build(BuildContext context) { | ||
| 201 | + if (_errorMsg != null) { | ||
| 202 | + return Center(child: Text(_errorMsg!)); | ||
| 203 | + } | ||
| 204 | + if (_localStream == null) { | ||
| 205 | + return const Center(child: CircularProgressIndicator()); | ||
| 206 | + } | ||
| 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 | + | ||
| 220 | + 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 | + }); | ||
| 242 | + } | ||
| 243 | +} | ||
| 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 | + ); |
lib/src/web/jsqr.dart
0 → 100644
lib/src/web/media.dart
0 → 100644
| 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 | + | ||
| 4 | +@JS('navigator.mediaDevices') | ||
| 5 | +library media_devices; | ||
| 6 | + | ||
| 7 | +import 'package:js/js.dart'; | ||
| 8 | + | ||
| 9 | +@JS('getUserMedia') | ||
| 10 | +external Future<dynamic> getUserMedia(UserMediaOptions constraints); | ||
| 11 | + | ||
| 12 | +@JS() | ||
| 13 | +@anonymous | ||
| 14 | +class UserMediaOptions { | ||
| 15 | + external VideoOptions get video; | ||
| 16 | + | ||
| 17 | + external factory UserMediaOptions({VideoOptions? video}); | ||
| 18 | +} | ||
| 19 | + | ||
| 20 | +@JS() | ||
| 21 | +@anonymous | ||
| 22 | +class VideoOptions { | ||
| 23 | + external String get facingMode; | ||
| 24 | + // external DeviceIdOptions get deviceId; | ||
| 25 | + | ||
| 26 | + external factory VideoOptions( | ||
| 27 | + {String? facingMode, DeviceIdOptions? deviceId}); | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | +@JS() | ||
| 31 | +@anonymous | ||
| 32 | +class DeviceIdOptions { | ||
| 33 | + external String get exact; | ||
| 34 | + | ||
| 35 | + external factory DeviceIdOptions({String? exact}); | ||
| 36 | +} |
-
Please register or login to post a comment