| // 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; |
| 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 _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 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 constraints = await 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 mediaDevices = window?.navigator.mediaDevices; |
| final supportedConstraints = mediaDevices?.getSupportedConstraints(); |
| final zoomLevelSupported = |
| supportedConstraints?[ZoomLevelCapability.constraintName] ?? false; |
| |
| if (!zoomLevelSupported) { |
| throw CameraWebException( |
| camera.textureId, |
| CameraErrorCode.zoomLevelNotSupported, |
| 'The zoom level is not supported in the current browser.', |
| ); |
| } |
| |
| final videoTracks = camera.stream?.getVideoTracks() ?? []; |
| |
| if (videoTracks.isNotEmpty) { |
| final defaultVideoTrack = videoTracks.first; |
| |
| /// The zoom level capability is represented by MediaSettingsRange. |
| /// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaSettingsRange |
| final zoomLevelCapability = defaultVideoTrack |
| .getCapabilities()[ZoomLevelCapability.constraintName] ?? |
| {}; |
| |
| // 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 minimumZoomLevel = jsUtil.getProperty(zoomLevelCapability, 'min'); |
| final maximumZoomLevel = jsUtil.getProperty(zoomLevelCapability, 'max'); |
| |
| 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 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 supportedConstraints = mediaDevices.getSupportedConstraints(); |
| final facingModeSupported = supportedConstraints[_facingModeKey] ?? 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 videoTrackSettings = videoTrack.getSettings(); |
| final facingMode = videoTrackSettings[_facingModeKey]; |
| |
| 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 videoTrackCapabilities = videoTrack.getCapabilities(); |
| |
| // A list of facing mode capabilities as |
| // the camera may support multiple facing modes. |
| final facingModeCapabilities = |
| List<String>.from(videoTrackCapabilities[_facingModeKey] ?? []); |
| |
| if (facingModeCapabilities.isNotEmpty) { |
| final 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 Size(4096, 2160); |
| case ResolutionPreset.veryHigh: |
| return Size(1920, 1080); |
| case ResolutionPreset.high: |
| return Size(1280, 720); |
| case ResolutionPreset.medium: |
| return Size(720, 480); |
| case ResolutionPreset.low: |
| default: |
| return 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; |
| } |
| } |
| } |