Navaron Bracke

move media stream track creation to the MobileScannerWeb implementation instead …

…of inside the barcode reader
@@ -111,13 +111,15 @@ abstract class BarcodeReader { @@ -111,13 +111,15 @@ abstract class BarcodeReader {
111 throw UnimplementedError('setTorchState() has not been implemented.'); 111 throw UnimplementedError('setTorchState() has not been implemented.');
112 } 112 }
113 113
114 - /// Start the barcode reader and initialize the video stream. 114 + /// Start the barcode reader and initialize the [videoStream].
115 /// 115 ///
116 /// The [options] are used to configure the barcode reader. 116 /// The [options] are used to configure the barcode reader.
117 /// The [containerElement] will become the parent of the video output element. 117 /// The [containerElement] will become the parent of the video output element.
  118 + /// The [videoStream] is the input for the barcode reader and video preview element.
118 Future<void> start( 119 Future<void> start(
119 StartOptions options, { 120 StartOptions options, {
120 required HTMLElement containerElement, 121 required HTMLElement containerElement,
  122 + required MediaStream videoStream,
121 }) { 123 }) {
122 throw UnimplementedError('start() has not been implemented.'); 124 throw UnimplementedError('start() has not been implemented.');
123 } 125 }
1 import 'dart:async'; 1 import 'dart:async';
  2 +import 'dart:js_interop';
2 import 'dart:ui_web' as ui_web; 3 import 'dart:ui_web' as ui_web;
3 4
4 import 'package:flutter/widgets.dart'; 5 import 'package:flutter/widgets.dart';
5 import 'package:flutter_web_plugins/flutter_web_plugins.dart'; 6 import 'package:flutter_web_plugins/flutter_web_plugins.dart';
  7 +import 'package:mobile_scanner/src/enums/camera_facing.dart';
6 import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; 8 import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
7 import 'package:mobile_scanner/src/enums/torch_state.dart'; 9 import 'package:mobile_scanner/src/enums/torch_state.dart';
8 import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; 10 import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
@@ -69,6 +71,93 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -69,6 +71,93 @@ class MobileScannerWeb extends MobileScannerPlatform {
69 _settingsController.add(settings); 71 _settingsController.add(settings);
70 } 72 }
71 73
  74 + /// Prepare a [MediaStream] for the video output.
  75 + ///
  76 + /// This method requests permission to use the camera.
  77 + ///
  78 + /// Throws a [MobileScannerException] if the permission was denied,
  79 + /// or if using a video stream, with the given set of constraints, is unsupported.
  80 + Future<MediaStream> _prepareVideoStream(
  81 + CameraFacing cameraDirection,
  82 + ) async {
  83 + if ((window.navigator.mediaDevices as JSAny?).isUndefinedOrNull) {
  84 + throw const MobileScannerException(
  85 + errorCode: MobileScannerErrorCode.unsupported,
  86 + errorDetails: MobileScannerErrorDetails(
  87 + message: 'This browser does not support displaying video from the camera.',
  88 + ),
  89 + );
  90 + }
  91 +
  92 + final MediaTrackSupportedConstraints capabilities = window.navigator.mediaDevices.getSupportedConstraints();
  93 +
  94 + final MediaStreamConstraints constraints;
  95 +
  96 + if ((capabilities as JSAny).isUndefinedOrNull || !capabilities.facingMode) {
  97 + constraints = MediaStreamConstraints(video: true.toJS);
  98 + } else {
  99 + final String facingMode = switch (cameraDirection) {
  100 + CameraFacing.back => 'environment',
  101 + CameraFacing.front => 'user',
  102 + };
  103 +
  104 + constraints = MediaStreamConstraints(
  105 + video: MediaTrackConstraintSet(facingMode: facingMode.toJS) as JSAny,
  106 + );
  107 + }
  108 +
  109 + try {
  110 + // Retrieving the video track requests the camera permission.
  111 + // If the completer is not null, the permission was never requested before.
  112 + _cameraPermissionCompleter ??= Completer<void>();
  113 +
  114 + final MediaStream? videoStream =
  115 + await window.navigator.mediaDevices.getUserMedia(constraints).toDart as MediaStream?;
  116 +
  117 + // At this point the permission is granted.
  118 + if (!_cameraPermissionCompleter!.isCompleted) {
  119 + _cameraPermissionCompleter!.complete();
  120 + }
  121 +
  122 + if (videoStream == null) {
  123 + throw const MobileScannerException(
  124 + errorCode: MobileScannerErrorCode.genericError,
  125 + errorDetails: MobileScannerErrorDetails(
  126 + message: 'Could not create a video stream from the camera with the given options. '
  127 + 'The browser might not support the given constraints.',
  128 + ),
  129 + );
  130 + }
  131 +
  132 + return videoStream;
  133 + } on DOMException catch (error, stackTrace) {
  134 + // At this point the permission request completed, although with an error,
  135 + // but the error is irrelevant for the completer.
  136 + if (!_cameraPermissionCompleter!.isCompleted) {
  137 + _cameraPermissionCompleter!.complete();
  138 + }
  139 +
  140 + final String errorMessage = error.toString();
  141 +
  142 + MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;
  143 +
  144 + // Handle both unsupported and permission errors from the web.
  145 + if (errorMessage.contains('NotFoundError') || errorMessage.contains('NotSupportedError')) {
  146 + errorCode = MobileScannerErrorCode.unsupported;
  147 + } else if (errorMessage.contains('NotAllowedError')) {
  148 + errorCode = MobileScannerErrorCode.permissionDenied;
  149 + }
  150 +
  151 + throw MobileScannerException(
  152 + errorCode: errorCode,
  153 + errorDetails: MobileScannerErrorDetails(
  154 + message: errorMessage,
  155 + details: stackTrace.toString(),
  156 + ),
  157 + );
  158 + }
  159 + }
  160 +
72 @override 161 @override
73 Widget buildCameraView() { 162 Widget buildCameraView() {
74 if (!_barcodeReader.isScanning) { 163 if (!_barcodeReader.isScanning) {
@@ -111,6 +200,11 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -111,6 +200,11 @@ class MobileScannerWeb extends MobileScannerPlatform {
111 ); 200 );
112 } 201 }
113 202
  203 + // Request camera permissions and prepare the video stream.
  204 + final MediaStream videoStream = await _prepareVideoStream(
  205 + startOptions.cameraDirection,
  206 + );
  207 +
114 try { 208 try {
115 // Clear the existing barcodes. 209 // Clear the existing barcodes.
116 if (!_barcodesController.isClosed) { 210 if (!_barcodesController.isClosed) {
@@ -125,25 +219,13 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -125,25 +219,13 @@ class MobileScannerWeb extends MobileScannerPlatform {
125 await _barcodeReader.start( 219 await _barcodeReader.start(
126 startOptions, 220 startOptions,
127 containerElement: _divElement!, 221 containerElement: _divElement!,
  222 + videoStream: videoStream,
128 ); 223 );
129 } catch (error, stackTrace) { 224 } catch (error, stackTrace) {
130 - final String errorMessage = error.toString();  
131 -  
132 - MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;  
133 -  
134 - if (error is DOMException) {  
135 - if (errorMessage.contains('NotFoundError') ||  
136 - errorMessage.contains('NotSupportedError')) {  
137 - errorCode = MobileScannerErrorCode.unsupported;  
138 - } else if (errorMessage.contains('NotAllowedError')) {  
139 - errorCode = MobileScannerErrorCode.permissionDenied;  
140 - }  
141 - }  
142 -  
143 throw MobileScannerException( 225 throw MobileScannerException(
144 - errorCode: errorCode, 226 + errorCode: MobileScannerErrorCode.genericError,
145 errorDetails: MobileScannerErrorDetails( 227 errorDetails: MobileScannerErrorDetails(
146 - message: errorMessage, 228 + message: error.toString(),
147 details: stackTrace.toString(), 229 details: stackTrace.toString(),
148 ), 230 ),
149 ); 231 );
@@ -3,7 +3,6 @@ import 'dart:js_interop'; @@ -3,7 +3,6 @@ import 'dart:js_interop';
3 import 'dart:ui'; 3 import 'dart:ui';
4 4
5 import 'package:mobile_scanner/src/enums/barcode_format.dart'; 5 import 'package:mobile_scanner/src/enums/barcode_format.dart';
6 -import 'package:mobile_scanner/src/enums/camera_facing.dart';  
7 import 'package:mobile_scanner/src/enums/torch_state.dart'; 6 import 'package:mobile_scanner/src/enums/torch_state.dart';
8 import 'package:mobile_scanner/src/objects/barcode_capture.dart'; 7 import 'package:mobile_scanner/src/objects/barcode_capture.dart';
9 import 'package:mobile_scanner/src/objects/start_options.dart'; 8 import 'package:mobile_scanner/src/objects/start_options.dart';
@@ -106,43 +105,6 @@ final class ZXingBarcodeReader extends BarcodeReader { @@ -106,43 +105,6 @@ final class ZXingBarcodeReader extends BarcodeReader {
106 return hints; 105 return hints;
107 } 106 }
108 107
109 - /// Prepare the [web.MediaStream] for the barcode reader video input.  
110 - ///  
111 - /// This method requests permission to use the camera.  
112 - Future<web.MediaStream?> _prepareMediaStream(  
113 - CameraFacing cameraDirection,  
114 - ) async {  
115 - if ((web.window.navigator.mediaDevices as JSAny?).isUndefinedOrNull) {  
116 - return null;  
117 - }  
118 -  
119 - final capabilities =  
120 - web.window.navigator.mediaDevices.getSupportedConstraints();  
121 -  
122 - final web.MediaStreamConstraints constraints;  
123 -  
124 - if ((capabilities as JSAny).isUndefinedOrNull || !capabilities.facingMode) {  
125 - constraints = web.MediaStreamConstraints(video: true.toJS);  
126 - } else {  
127 - final String facingMode = switch (cameraDirection) {  
128 - CameraFacing.back => 'environment',  
129 - CameraFacing.front => 'user',  
130 - };  
131 -  
132 - constraints = web.MediaStreamConstraints(  
133 - video: web.MediaTrackConstraintSet(  
134 - facingMode: facingMode.toJS,  
135 - ) as JSAny,  
136 - );  
137 - }  
138 -  
139 - final JSAny? mediaStream = await web.window.navigator.mediaDevices  
140 - .getUserMedia(constraints)  
141 - .toDart;  
142 -  
143 - return mediaStream as web.MediaStream?;  
144 - }  
145 -  
146 /// Prepare the video element for the barcode reader. 108 /// Prepare the video element for the barcode reader.
147 /// 109 ///
148 /// The given [videoElement] is attached to the DOM, by attaching it to the [containerElement]. 110 /// The given [videoElement] is attached to the DOM, by attaching it to the [containerElement].
@@ -150,31 +112,25 @@ final class ZXingBarcodeReader extends BarcodeReader { @@ -150,31 +112,25 @@ final class ZXingBarcodeReader extends BarcodeReader {
150 /// and the video element (to display the camera output). 112 /// and the video element (to display the camera output).
151 Future<void> _prepareVideoElement( 113 Future<void> _prepareVideoElement(
152 web.HTMLVideoElement videoElement, { 114 web.HTMLVideoElement videoElement, {
153 - required CameraFacing cameraDirection, 115 + required web.MediaStream videoStream,
154 required web.HTMLElement containerElement, 116 required web.HTMLElement containerElement,
155 }) async { 117 }) async {
156 // Attach the video element to the DOM, through its parent container. 118 // Attach the video element to the DOM, through its parent container.
157 containerElement.appendChild(videoElement); 119 containerElement.appendChild(videoElement);
158 120
159 - // Set up the camera output stream.  
160 - // This will request permission to use the camera.  
161 - final web.MediaStream? stream = await _prepareMediaStream(cameraDirection);  
162 -  
163 - if (stream != null) {  
164 - final JSPromise? result = _reader?.attachStreamToVideo.callAsFunction(  
165 - _reader as JSAny?,  
166 - stream as JSAny,  
167 - videoElement as JSAny,  
168 - ) as JSPromise?; 121 + final JSPromise? result = _reader?.attachStreamToVideo.callAsFunction(
  122 + _reader as JSAny?,
  123 + videoStream as JSAny,
  124 + videoElement as JSAny,
  125 + ) as JSPromise?;
169 126
170 - await result?.toDart; 127 + await result?.toDart;
171 128
172 - final web.MediaTrackSettings? settings =  
173 - _mediaTrackConstraintsDelegate.getSettings(stream); 129 + final web.MediaTrackSettings? settings =
  130 + _mediaTrackConstraintsDelegate.getSettings(videoStream);
174 131
175 - if (settings != null) {  
176 - _onMediaTrackSettingsChanged?.call(settings);  
177 - } 132 + if (settings != null) {
  133 + _onMediaTrackSettingsChanged?.call(settings);
178 } 134 }
179 } 135 }
180 136
@@ -261,6 +217,7 @@ final class ZXingBarcodeReader extends BarcodeReader { @@ -261,6 +217,7 @@ final class ZXingBarcodeReader extends BarcodeReader {
261 Future<void> start( 217 Future<void> start(
262 StartOptions options, { 218 StartOptions options, {
263 required web.HTMLElement containerElement, 219 required web.HTMLElement containerElement,
  220 + required web.MediaStream videoStream,
264 }) async { 221 }) async {
265 final int detectionTimeoutMs = options.detectionTimeoutMs; 222 final int detectionTimeoutMs = options.detectionTimeoutMs;
266 final List<BarcodeFormat> formats = options.formats; 223 final List<BarcodeFormat> formats = options.formats;
@@ -279,7 +236,7 @@ final class ZXingBarcodeReader extends BarcodeReader { @@ -279,7 +236,7 @@ final class ZXingBarcodeReader extends BarcodeReader {
279 236
280 await _prepareVideoElement( 237 await _prepareVideoElement(
281 videoElement, 238 videoElement,
282 - cameraDirection: options.cameraDirection, 239 + videoStream: videoStream,
283 containerElement: containerElement, 240 containerElement: containerElement,
284 ); 241 );
285 } 242 }