Navaron Bracke

move media stream track creation to the MobileScannerWeb implementation instead …

…of inside the barcode reader
... ... @@ -111,13 +111,15 @@ abstract class BarcodeReader {
throw UnimplementedError('setTorchState() has not been implemented.');
}
/// Start the barcode reader and initialize the video stream.
/// Start the barcode reader and initialize the [videoStream].
///
/// The [options] are used to configure the barcode reader.
/// The [containerElement] will become the parent of the video output element.
/// The [videoStream] is the input for the barcode reader and video preview element.
Future<void> start(
StartOptions options, {
required HTMLElement containerElement,
required MediaStream videoStream,
}) {
throw UnimplementedError('start() has not been implemented.');
}
... ...
import 'dart:async';
import 'dart:js_interop';
import 'dart:ui_web' as ui_web;
import 'package:flutter/widgets.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:mobile_scanner/src/enums/camera_facing.dart';
import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
import 'package:mobile_scanner/src/enums/torch_state.dart';
import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
... ... @@ -69,6 +71,93 @@ class MobileScannerWeb extends MobileScannerPlatform {
_settingsController.add(settings);
}
/// Prepare a [MediaStream] for the video output.
///
/// This method requests permission to use the camera.
///
/// Throws a [MobileScannerException] if the permission was denied,
/// or if using a video stream, with the given set of constraints, is unsupported.
Future<MediaStream> _prepareVideoStream(
CameraFacing cameraDirection,
) async {
if ((window.navigator.mediaDevices as JSAny?).isUndefinedOrNull) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.unsupported,
errorDetails: MobileScannerErrorDetails(
message: 'This browser does not support displaying video from the camera.',
),
);
}
final MediaTrackSupportedConstraints capabilities = window.navigator.mediaDevices.getSupportedConstraints();
final MediaStreamConstraints constraints;
if ((capabilities as JSAny).isUndefinedOrNull || !capabilities.facingMode) {
constraints = MediaStreamConstraints(video: true.toJS);
} else {
final String facingMode = switch (cameraDirection) {
CameraFacing.back => 'environment',
CameraFacing.front => 'user',
};
constraints = MediaStreamConstraints(
video: MediaTrackConstraintSet(facingMode: facingMode.toJS) as JSAny,
);
}
try {
// Retrieving the video track requests the camera permission.
// If the completer is not null, the permission was never requested before.
_cameraPermissionCompleter ??= Completer<void>();
final MediaStream? videoStream =
await window.navigator.mediaDevices.getUserMedia(constraints).toDart as MediaStream?;
// At this point the permission is granted.
if (!_cameraPermissionCompleter!.isCompleted) {
_cameraPermissionCompleter!.complete();
}
if (videoStream == null) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
message: 'Could not create a video stream from the camera with the given options. '
'The browser might not support the given constraints.',
),
);
}
return videoStream;
} on DOMException catch (error, stackTrace) {
// At this point the permission request completed, although with an error,
// but the error is irrelevant for the completer.
if (!_cameraPermissionCompleter!.isCompleted) {
_cameraPermissionCompleter!.complete();
}
final String errorMessage = error.toString();
MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;
// Handle both unsupported and permission errors from the web.
if (errorMessage.contains('NotFoundError') || errorMessage.contains('NotSupportedError')) {
errorCode = MobileScannerErrorCode.unsupported;
} else if (errorMessage.contains('NotAllowedError')) {
errorCode = MobileScannerErrorCode.permissionDenied;
}
throw MobileScannerException(
errorCode: errorCode,
errorDetails: MobileScannerErrorDetails(
message: errorMessage,
details: stackTrace.toString(),
),
);
}
}
@override
Widget buildCameraView() {
if (!_barcodeReader.isScanning) {
... ... @@ -111,6 +200,11 @@ class MobileScannerWeb extends MobileScannerPlatform {
);
}
// Request camera permissions and prepare the video stream.
final MediaStream videoStream = await _prepareVideoStream(
startOptions.cameraDirection,
);
try {
// Clear the existing barcodes.
if (!_barcodesController.isClosed) {
... ... @@ -125,25 +219,13 @@ class MobileScannerWeb extends MobileScannerPlatform {
await _barcodeReader.start(
startOptions,
containerElement: _divElement!,
videoStream: videoStream,
);
} catch (error, stackTrace) {
final String errorMessage = error.toString();
MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;
if (error is DOMException) {
if (errorMessage.contains('NotFoundError') ||
errorMessage.contains('NotSupportedError')) {
errorCode = MobileScannerErrorCode.unsupported;
} else if (errorMessage.contains('NotAllowedError')) {
errorCode = MobileScannerErrorCode.permissionDenied;
}
}
throw MobileScannerException(
errorCode: errorCode,
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
message: errorMessage,
message: error.toString(),
details: stackTrace.toString(),
),
);
... ...
... ... @@ -3,7 +3,6 @@ import 'dart:js_interop';
import 'dart:ui';
import 'package:mobile_scanner/src/enums/barcode_format.dart';
import 'package:mobile_scanner/src/enums/camera_facing.dart';
import 'package:mobile_scanner/src/enums/torch_state.dart';
import 'package:mobile_scanner/src/objects/barcode_capture.dart';
import 'package:mobile_scanner/src/objects/start_options.dart';
... ... @@ -106,43 +105,6 @@ final class ZXingBarcodeReader extends BarcodeReader {
return hints;
}
/// Prepare the [web.MediaStream] for the barcode reader video input.
///
/// This method requests permission to use the camera.
Future<web.MediaStream?> _prepareMediaStream(
CameraFacing cameraDirection,
) async {
if ((web.window.navigator.mediaDevices as JSAny?).isUndefinedOrNull) {
return null;
}
final capabilities =
web.window.navigator.mediaDevices.getSupportedConstraints();
final web.MediaStreamConstraints constraints;
if ((capabilities as JSAny).isUndefinedOrNull || !capabilities.facingMode) {
constraints = web.MediaStreamConstraints(video: true.toJS);
} else {
final String facingMode = switch (cameraDirection) {
CameraFacing.back => 'environment',
CameraFacing.front => 'user',
};
constraints = web.MediaStreamConstraints(
video: web.MediaTrackConstraintSet(
facingMode: facingMode.toJS,
) as JSAny,
);
}
final JSAny? mediaStream = await web.window.navigator.mediaDevices
.getUserMedia(constraints)
.toDart;
return mediaStream as web.MediaStream?;
}
/// Prepare the video element for the barcode reader.
///
/// The given [videoElement] is attached to the DOM, by attaching it to the [containerElement].
... ... @@ -150,31 +112,25 @@ final class ZXingBarcodeReader extends BarcodeReader {
/// and the video element (to display the camera output).
Future<void> _prepareVideoElement(
web.HTMLVideoElement videoElement, {
required CameraFacing cameraDirection,
required web.MediaStream videoStream,
required web.HTMLElement containerElement,
}) async {
// Attach the video element to the DOM, through its parent container.
containerElement.appendChild(videoElement);
// Set up the camera output stream.
// This will request permission to use the camera.
final web.MediaStream? stream = await _prepareMediaStream(cameraDirection);
if (stream != null) {
final JSPromise? result = _reader?.attachStreamToVideo.callAsFunction(
_reader as JSAny?,
stream as JSAny,
videoElement as JSAny,
) as JSPromise?;
final JSPromise? result = _reader?.attachStreamToVideo.callAsFunction(
_reader as JSAny?,
videoStream as JSAny,
videoElement as JSAny,
) as JSPromise?;
await result?.toDart;
await result?.toDart;
final web.MediaTrackSettings? settings =
_mediaTrackConstraintsDelegate.getSettings(stream);
final web.MediaTrackSettings? settings =
_mediaTrackConstraintsDelegate.getSettings(videoStream);
if (settings != null) {
_onMediaTrackSettingsChanged?.call(settings);
}
if (settings != null) {
_onMediaTrackSettingsChanged?.call(settings);
}
}
... ... @@ -261,6 +217,7 @@ final class ZXingBarcodeReader extends BarcodeReader {
Future<void> start(
StartOptions options, {
required web.HTMLElement containerElement,
required web.MediaStream videoStream,
}) async {
final int detectionTimeoutMs = options.detectionTimeoutMs;
final List<BarcodeFormat> formats = options.formats;
... ... @@ -279,7 +236,7 @@ final class ZXingBarcodeReader extends BarcodeReader {
await _prepareVideoElement(
videoElement,
cameraDirection: options.cameraDirection,
videoStream: videoStream,
containerElement: containerElement,
);
}
... ...