| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:html' as html; |
| // TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) |
| // ignore: unnecessary_import |
| import 'dart:ui'; |
| |
| import 'package:camera_platform_interface/camera_platform_interface.dart'; |
| import 'package:camera_web/src/camera.dart'; |
| import 'package:camera_web/src/shims/dart_js_util.dart'; |
| import 'package:camera_web/src/types/types.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/services.dart'; |
| |
| /// A service to fetch, map camera settings and |
| /// obtain the camera stream. |
| class CameraService { |
| // A facing mode constraint name. |
| static const String _facingModeKey = 'facingMode'; |
| |
| /// The current browser window used to access media devices. |
| @visibleForTesting |
| html.Window? window = html.window; |
| |
| /// The utility to manipulate JavaScript interop objects. |
| @visibleForTesting |
| JsUtil jsUtil = JsUtil(); |
| |
| /// Returns a media stream associated with the camera device |
| /// with [cameraId] and constrained by [options]. |
| Future<html.MediaStream> getMediaStreamForOptions( |
| CameraOptions options, { |
| int cameraId = 0, |
| }) async { |
| final html.MediaDevices? mediaDevices = window?.navigator.mediaDevices; |
| |
| // Throw a not supported exception if the current browser window |
| // does not support any media devices. |
| if (mediaDevices == null) { |
| throw PlatformException( |
| code: CameraErrorCode.notSupported.toString(), |
| message: 'The camera is not supported on this device.', |
| ); |
| } |
| |
| try { |
| final Map<String, dynamic> constraints = options.toJson(); |
| return await mediaDevices.getUserMedia(constraints); |
| } on html.DomException catch (e) { |
| switch (e.name) { |
| case 'NotFoundError': |
| case 'DevicesNotFoundError': |
| throw CameraWebException( |
| cameraId, |
| CameraErrorCode.notFound, |
| 'No camera found for the given camera options.', |
| ); |
| case 'NotReadableError': |
| case 'TrackStartError': |
| throw CameraWebException( |
| cameraId, |
| CameraErrorCode.notReadable, |
| 'The camera is not readable due to a hardware error ' |
| 'that prevented access to the device.', |
| ); |
| case 'OverconstrainedError': |
| case 'ConstraintNotSatisfiedError': |
| throw CameraWebException( |
| cameraId, |
| CameraErrorCode.overconstrained, |
| 'The camera options are impossible to satisfy.', |
| ); |
| case 'NotAllowedError': |
| case 'PermissionDeniedError': |
| throw CameraWebException( |
| cameraId, |
| CameraErrorCode.permissionDenied, |
| 'The camera cannot be used or the permission ' |
| 'to access the camera is not granted.', |
| ); |
| case 'TypeError': |
| throw CameraWebException( |
| cameraId, |
| CameraErrorCode.type, |
| 'The camera options are incorrect or attempted ' |
| 'to access the media input from an insecure context.', |
| ); |
| case 'AbortError': |
| throw CameraWebException( |
| cameraId, |
| CameraErrorCode.abort, |
| 'Some problem occurred that prevented the camera from being used.', |
| ); |
| case 'SecurityError': |
| throw CameraWebException( |
| cameraId, |
| CameraErrorCode.security, |
| 'The user media support is disabled in the current browser.', |
| ); |
| default: |
| throw CameraWebException( |
| cameraId, |
| CameraErrorCode.unknown, |
| 'An unknown error occured when fetching the camera stream.', |
| ); |
| } |
| } catch (_) { |
| throw CameraWebException( |
| cameraId, |
| CameraErrorCode.unknown, |
| 'An unknown error occured when fetching the camera stream.', |
| ); |
| } |
| } |
| |
| /// Returns the zoom level capability for the given [camera]. |
| /// |
| /// Throws a [CameraWebException] if the zoom level is not supported |
| /// or the camera has not been initialized or started. |
| ZoomLevelCapability getZoomLevelCapabilityForCamera( |
| Camera camera, |
| ) { |
| final html.MediaDevices? mediaDevices = window?.navigator.mediaDevices; |
| final Map<dynamic, dynamic>? supportedConstraints = |
| mediaDevices?.getSupportedConstraints(); |
| final bool zoomLevelSupported = |
| supportedConstraints?[ZoomLevelCapability.constraintName] as bool? ?? |
| false; |
| |
| if (!zoomLevelSupported) { |
| throw CameraWebException( |
| camera.textureId, |
| CameraErrorCode.zoomLevelNotSupported, |
| 'The zoom level is not supported in the current browser.', |
| ); |
| } |
| |
| final List<html.MediaStreamTrack> videoTracks = |
| camera.stream?.getVideoTracks() ?? <html.MediaStreamTrack>[]; |
| |
| if (videoTracks.isNotEmpty) { |
| final html.MediaStreamTrack defaultVideoTrack = videoTracks.first; |
| |
| /// The zoom level capability is represented by MediaSettingsRange. |
| /// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaSettingsRange |
| final Object zoomLevelCapability = defaultVideoTrack |
| .getCapabilities()[ZoomLevelCapability.constraintName] |
| as Object? ?? |
| <dynamic, dynamic>{}; |
| |
| // The zoom level capability is a nested JS object, therefore |
| // we need to access its properties with the js_util library. |
| // See: https://api.dart.dev/stable/2.13.4/dart-js_util/getProperty.html |
| final num? minimumZoomLevel = |
| jsUtil.getProperty(zoomLevelCapability, 'min') as num?; |
| final num? maximumZoomLevel = |
| jsUtil.getProperty(zoomLevelCapability, 'max') as num?; |
| |
| if (minimumZoomLevel != null && maximumZoomLevel != null) { |
| return ZoomLevelCapability( |
| minimum: minimumZoomLevel.toDouble(), |
| maximum: maximumZoomLevel.toDouble(), |
| videoTrack: defaultVideoTrack, |
| ); |
| } else { |
| throw CameraWebException( |
| camera.textureId, |
| CameraErrorCode.zoomLevelNotSupported, |
| 'The zoom level is not supported by the current camera.', |
| ); |
| } |
| } else { |
| throw CameraWebException( |
| camera.textureId, |
| CameraErrorCode.notStarted, |
| 'The camera has not been initialized or started.', |
| ); |
| } |
| } |
| |
| /// Returns a facing mode of the [videoTrack] |
| /// (null if the facing mode is not available). |
| String? getFacingModeForVideoTrack(html.MediaStreamTrack videoTrack) { |
| final html.MediaDevices? mediaDevices = window?.navigator.mediaDevices; |
| |
| // Throw a not supported exception if the current browser window |
| // does not support any media devices. |
| if (mediaDevices == null) { |
| throw PlatformException( |
| code: CameraErrorCode.notSupported.toString(), |
| message: 'The camera is not supported on this device.', |
| ); |
| } |
| |
| // Check if the camera facing mode is supported by the current browser. |
| final Map<dynamic, dynamic> supportedConstraints = |
| mediaDevices.getSupportedConstraints(); |
| final bool facingModeSupported = |
| supportedConstraints[_facingModeKey] as bool? ?? false; |
| |
| // Return null if the facing mode is not supported. |
| if (!facingModeSupported) { |
| return null; |
| } |
| |
| // Extract the facing mode from the video track settings. |
| // The property may not be available if it's not supported |
| // by the browser or not available due to context. |
| // |
| // MediaTrackSettings: |
| // https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings |
| final Map<dynamic, dynamic> videoTrackSettings = videoTrack.getSettings(); |
| final String? facingMode = videoTrackSettings[_facingModeKey] as String?; |
| |
| if (facingMode == null) { |
| // If the facing mode does not exist in the video track settings, |
| // check for the facing mode in the video track capabilities. |
| // |
| // MediaTrackCapabilities: |
| // https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackcapabilities |
| |
| // Check if getting the video track capabilities is supported. |
| // |
| // The method may not be supported on Firefox. |
| // See: https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/getCapabilities#browser_compatibility |
| if (!jsUtil.hasProperty(videoTrack, 'getCapabilities')) { |
| // Return null if the video track capabilites are not supported. |
| return null; |
| } |
| |
| final Map<dynamic, dynamic> videoTrackCapabilities = |
| videoTrack.getCapabilities(); |
| |
| // A list of facing mode capabilities as |
| // the camera may support multiple facing modes. |
| final List<String> facingModeCapabilities = List<String>.from( |
| (videoTrackCapabilities[_facingModeKey] as List<dynamic>?) |
| ?.cast<String>() ?? |
| <String>[]); |
| |
| if (facingModeCapabilities.isNotEmpty) { |
| final String facingModeCapability = facingModeCapabilities.first; |
| return facingModeCapability; |
| } else { |
| // Return null if there are no facing mode capabilities. |
| return null; |
| } |
| } |
| |
| return facingMode; |
| } |
| |
| /// Maps the given [facingMode] to [CameraLensDirection]. |
| /// |
| /// The following values for the facing mode are supported: |
| /// https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings/facingMode |
| CameraLensDirection mapFacingModeToLensDirection(String facingMode) { |
| switch (facingMode) { |
| case 'user': |
| return CameraLensDirection.front; |
| case 'environment': |
| return CameraLensDirection.back; |
| case 'left': |
| case 'right': |
| default: |
| return CameraLensDirection.external; |
| } |
| } |
| |
| /// Maps the given [facingMode] to [CameraType]. |
| /// |
| /// See [CameraMetadata.facingMode] for more details. |
| CameraType mapFacingModeToCameraType(String facingMode) { |
| switch (facingMode) { |
| case 'user': |
| return CameraType.user; |
| case 'environment': |
| return CameraType.environment; |
| case 'left': |
| case 'right': |
| default: |
| return CameraType.user; |
| } |
| } |
| |
| /// Maps the given [resolutionPreset] to [Size]. |
| Size mapResolutionPresetToSize(ResolutionPreset resolutionPreset) { |
| switch (resolutionPreset) { |
| case ResolutionPreset.max: |
| case ResolutionPreset.ultraHigh: |
| return const Size(4096, 2160); |
| case ResolutionPreset.veryHigh: |
| return const Size(1920, 1080); |
| case ResolutionPreset.high: |
| return const Size(1280, 720); |
| case ResolutionPreset.medium: |
| return const Size(720, 480); |
| case ResolutionPreset.low: |
| default: |
| return const Size(320, 240); |
| } |
| } |
| |
| /// Maps the given [deviceOrientation] to [OrientationType]. |
| String mapDeviceOrientationToOrientationType( |
| DeviceOrientation deviceOrientation, |
| ) { |
| switch (deviceOrientation) { |
| case DeviceOrientation.portraitUp: |
| return OrientationType.portraitPrimary; |
| case DeviceOrientation.landscapeLeft: |
| return OrientationType.landscapePrimary; |
| case DeviceOrientation.portraitDown: |
| return OrientationType.portraitSecondary; |
| case DeviceOrientation.landscapeRight: |
| return OrientationType.landscapeSecondary; |
| } |
| } |
| |
| /// Maps the given [orientationType] to [DeviceOrientation]. |
| DeviceOrientation mapOrientationTypeToDeviceOrientation( |
| String orientationType, |
| ) { |
| switch (orientationType) { |
| case OrientationType.portraitPrimary: |
| return DeviceOrientation.portraitUp; |
| case OrientationType.landscapePrimary: |
| return DeviceOrientation.landscapeLeft; |
| case OrientationType.portraitSecondary: |
| return DeviceOrientation.portraitDown; |
| case OrientationType.landscapeSecondary: |
| return DeviceOrientation.landscapeRight; |
| default: |
| return DeviceOrientation.portraitUp; |
| } |
| } |
| |
| CameraImageData getCameraImageDataFromBlob( |
| html.Blob blob, { |
| required int height, |
| required int width, |
| }) { |
| return CameraImageData( |
| format: const CameraImageFormat( |
| ImageFormatGroup.jpeg, |
| raw: '', |
| ), |
| planes: [], |
| height: height, |
| width: width, |
| ); |
| } |
| } |