Julian Steenbakker
Committed by GitHub

Merge branch 'master' into pavel/changelog-web

@@ -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,
@@ -93,11 +93,12 @@ class MobileScannerWebPlugin { @@ -93,11 +93,12 @@ class MobileScannerWebPlugin {
93 93
94 // Check if stream is running 94 // Check if stream is running
95 if (barCodeReader.isStarted) { 95 if (barCodeReader.isStarted) {
  96 + final hasTorch = await barCodeReader.hasTorch();
96 return { 97 return {
97 'ViewID': viewID, 98 'ViewID': viewID,
98 'videoWidth': barCodeReader.videoWidth, 99 'videoWidth': barCodeReader.videoWidth,
99 'videoHeight': barCodeReader.videoHeight, 100 'videoHeight': barCodeReader.videoHeight,
100 - 'torchable': barCodeReader.hasTorch, 101 + 'torchable': hasTorch,
101 }; 102 };
102 } 103 }
103 try { 104 try {
@@ -117,12 +118,17 @@ class MobileScannerWebPlugin { @@ -117,12 +118,17 @@ class MobileScannerWebPlugin {
117 }); 118 });
118 } 119 }
119 }); 120 });
  121 + final hasTorch = await barCodeReader.hasTorch();
  122 +
  123 + if (hasTorch && arguments.containsKey('torch')) {
  124 + barCodeReader.toggleTorch(enabled: arguments['torch'] as bool);
  125 + }
120 126
121 return { 127 return {
122 'ViewID': viewID, 128 'ViewID': viewID,
123 'videoWidth': barCodeReader.videoWidth, 129 'videoWidth': barCodeReader.videoWidth,
124 'videoHeight': barCodeReader.videoHeight, 130 'videoHeight': barCodeReader.videoHeight,
125 - 'torchable': barCodeReader.hasTorch, 131 + 'torchable': hasTorch,
126 }; 132 };
127 } catch (e) { 133 } catch (e) {
128 throw PlatformException(code: 'MobileScannerWeb', message: '$e'); 134 throw PlatformException(code: 'MobileScannerWeb', message: '$e');
@@ -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(
1 -import 'dart:html'; 1 +import 'dart:html' as html;
2 2
3 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
  4 +import 'package:js/js.dart';
  5 +import 'package:js/js_util.dart';
4 import 'package:mobile_scanner/src/enums/camera_facing.dart'; 6 import 'package:mobile_scanner/src/enums/camera_facing.dart';
5 import 'package:mobile_scanner/src/objects/barcode.dart'; 7 import 'package:mobile_scanner/src/objects/barcode.dart';
6 import 'package:mobile_scanner/src/web/media.dart'; 8 import 'package:mobile_scanner/src/web/media.dart';
@@ -8,7 +10,7 @@ import 'package:mobile_scanner/src/web/media.dart'; @@ -8,7 +10,7 @@ import 'package:mobile_scanner/src/web/media.dart';
8 abstract class WebBarcodeReaderBase { 10 abstract class WebBarcodeReaderBase {
9 /// Timer used to capture frames to be analyzed 11 /// Timer used to capture frames to be analyzed
10 final Duration frameInterval; 12 final Duration frameInterval;
11 - final DivElement videoContainer; 13 + final html.DivElement videoContainer;
12 14
13 const WebBarcodeReaderBase({ 15 const WebBarcodeReaderBase({
14 required this.videoContainer, 16 required this.videoContainer,
@@ -35,24 +37,24 @@ abstract class WebBarcodeReaderBase { @@ -35,24 +37,24 @@ abstract class WebBarcodeReaderBase {
35 Future<void> toggleTorch({required bool enabled}); 37 Future<void> toggleTorch({required bool enabled});
36 38
37 /// Determine whether device has flash 39 /// Determine whether device has flash
38 - bool get hasTorch; 40 + Future<bool> hasTorch();
39 } 41 }
40 42
41 mixin InternalStreamCreation on WebBarcodeReaderBase { 43 mixin InternalStreamCreation on WebBarcodeReaderBase {
42 /// The video stream. 44 /// The video stream.
43 /// Will be initialized later to see which camera needs to be used. 45 /// Will be initialized later to see which camera needs to be used.
44 - MediaStream? localMediaStream;  
45 - final VideoElement video = VideoElement(); 46 + html.MediaStream? localMediaStream;
  47 + final html.VideoElement video = html.VideoElement();
46 48
47 @override 49 @override
48 int get videoWidth => video.videoWidth; 50 int get videoWidth => video.videoWidth;
49 @override 51 @override
50 int get videoHeight => video.videoHeight; 52 int get videoHeight => video.videoHeight;
51 53
52 - Future<MediaStream?> initMediaStream(CameraFacing cameraFacing) async { 54 + Future<html.MediaStream?> initMediaStream(CameraFacing cameraFacing) async {
53 // Check if browser supports multiple camera's and set if supported 55 // Check if browser supports multiple camera's and set if supported
54 final Map? capabilities = 56 final Map? capabilities =
55 - window.navigator.mediaDevices?.getSupportedConstraints(); 57 + html.window.navigator.mediaDevices?.getSupportedConstraints();
56 final Map<String, dynamic> constraints; 58 final Map<String, dynamic> constraints;
57 if (capabilities != null && capabilities['facingMode'] as bool) { 59 if (capabilities != null && capabilities['facingMode'] as bool) {
58 constraints = { 60 constraints = {
@@ -65,15 +67,15 @@ mixin InternalStreamCreation on WebBarcodeReaderBase { @@ -65,15 +67,15 @@ mixin InternalStreamCreation on WebBarcodeReaderBase {
65 constraints = {'video': true}; 67 constraints = {'video': true};
66 } 68 }
67 final stream = 69 final stream =
68 - await window.navigator.mediaDevices?.getUserMedia(constraints); 70 + await html.window.navigator.mediaDevices?.getUserMedia(constraints);
69 return stream; 71 return stream;
70 } 72 }
71 73
72 - void prepareVideoElement(VideoElement videoSource); 74 + void prepareVideoElement(html.VideoElement videoSource);
73 75
74 Future<void> attachStreamToVideo( 76 Future<void> attachStreamToVideo(
75 - MediaStream stream,  
76 - VideoElement videoSource, 77 + html.MediaStream stream,
  78 + html.VideoElement videoSource,
77 ); 79 );
78 80
79 @override 81 @override
@@ -96,19 +98,34 @@ mixin InternalStreamCreation on WebBarcodeReaderBase { @@ -96,19 +98,34 @@ mixin InternalStreamCreation on WebBarcodeReaderBase {
96 98
97 /// Mixin for libraries that don't have built-in torch support 99 /// Mixin for libraries that don't have built-in torch support
98 mixin InternalTorchDetection on InternalStreamCreation { 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 +
99 @override 121 @override
100 - bool get hasTorch {  
101 - // TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533  
102 - // final track = _localStream?.getVideoTracks();  
103 - // if (track != null) {  
104 - // final imageCapture = html.ImageCapture(track.first);  
105 - // final photoCapabilities = await imageCapture.getPhotoCapabilities();  
106 - // }  
107 - return false; 122 + Future<bool> hasTorch() async {
  123 + return (await getSupportedTorchStates()).isNotEmpty;
108 } 124 }
109 125
110 @override 126 @override
111 Future<void> toggleTorch({required bool enabled}) async { 127 Future<void> toggleTorch({required bool enabled}) async {
  128 + final hasTorch = await this.hasTorch();
112 if (hasTorch) { 129 if (hasTorch) {
113 final track = localMediaStream?.getVideoTracks(); 130 final track = localMediaStream?.getVideoTracks();
114 await track?.first.applyConstraints({ 131 await track?.first.applyConstraints({
@@ -119,3 +136,25 @@ mixin InternalTorchDetection on InternalStreamCreation { @@ -119,3 +136,25 @@ mixin InternalTorchDetection on InternalStreamCreation {
119 } 136 }
120 } 137 }
121 } 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 +}
@@ -7,10 +7,6 @@ import 'package:mobile_scanner/src/enums/camera_facing.dart'; @@ -7,10 +7,6 @@ import 'package:mobile_scanner/src/enums/camera_facing.dart';
7 import 'package:mobile_scanner/src/objects/barcode.dart'; 7 import 'package:mobile_scanner/src/objects/barcode.dart';
8 import 'package:mobile_scanner/src/web/base.dart'; 8 import 'package:mobile_scanner/src/web/base.dart';
9 9
10 -@JS('Promise')  
11 -@staticInterop  
12 -class Promise<T> {}  
13 -  
14 @JS('ZXing.BrowserMultiFormatReader') 10 @JS('ZXing.BrowserMultiFormatReader')
15 @staticInterop 11 @staticInterop
16 class JsZXingBrowserMultiFormatReader { 12 class JsZXingBrowserMultiFormatReader {