move media stream track creation to the MobileScannerWeb implementation instead …
…of inside the barcode reader
Showing
3 changed files
with
113 additions
and
72 deletions
| @@ -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 | } |
-
Please register or login to post a comment