| // 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:async'; |
| import 'dart:html' as html; |
| import 'dart:math'; |
| |
| import 'package:camera_platform_interface/camera_platform_interface.dart'; |
| import 'package:camera_web/src/camera.dart'; |
| import 'package:camera_web/src/camera_settings.dart'; |
| import 'package:camera_web/src/types/types.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_web_plugins/flutter_web_plugins.dart'; |
| import 'package:stream_transform/stream_transform.dart'; |
| |
| /// The web implementation of [CameraPlatform]. |
| /// |
| /// This class implements the `package:camera` functionality for the web. |
| class CameraPlugin extends CameraPlatform { |
| /// Creates a new instance of [CameraPlugin] |
| /// with the given [cameraSettings] utility. |
| CameraPlugin({required CameraSettings cameraSettings}) |
| : _cameraSettings = cameraSettings; |
| |
| /// Registers this class as the default instance of [CameraPlatform]. |
| static void registerWith(Registrar registrar) { |
| CameraPlatform.instance = CameraPlugin( |
| cameraSettings: CameraSettings(), |
| ); |
| } |
| |
| final CameraSettings _cameraSettings; |
| |
| /// The cameras managed by the [CameraPlugin]. |
| @visibleForTesting |
| final cameras = <int, Camera>{}; |
| var _textureCounter = 1; |
| |
| /// Metadata associated with each camera description. |
| /// Populated in [availableCameras]. |
| @visibleForTesting |
| final camerasMetadata = <CameraDescription, CameraMetadata>{}; |
| |
| /// The controller used to broadcast different camera events. |
| /// |
| /// It is `broadcast` as multiple controllers may subscribe |
| /// to different stream views of this controller. |
| @visibleForTesting |
| final cameraEventStreamController = StreamController<CameraEvent>.broadcast(); |
| |
| /// Returns a stream of camera events for the given [cameraId]. |
| Stream<CameraEvent> _cameraEvents(int cameraId) => |
| cameraEventStreamController.stream |
| .where((event) => event.cameraId == cameraId); |
| |
| /// The current browser window used to access media devices. |
| @visibleForTesting |
| html.Window? window = html.window; |
| |
| @override |
| Future<List<CameraDescription>> availableCameras() async { |
| final mediaDevices = window?.navigator.mediaDevices; |
| final cameras = <CameraDescription>[]; |
| |
| // Throw a not supported exception if the current browser window |
| // does not support any media devices. |
| if (mediaDevices == null) { |
| throw CameraException( |
| CameraErrorCodes.notSupported, |
| 'The camera is not supported on this device.', |
| ); |
| } |
| |
| // Request available media devices. |
| final devices = await mediaDevices.enumerateDevices(); |
| |
| // Filter video input devices. |
| final videoInputDevices = devices |
| .whereType<html.MediaDeviceInfo>() |
| .where((device) => device.kind == MediaDeviceKind.videoInput) |
| |
| /// The device id property is currently not supported on Internet Explorer: |
| /// https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/deviceId#browser_compatibility |
| .where((device) => device.deviceId != null); |
| |
| // Map video input devices to camera descriptions. |
| for (final videoInputDevice in videoInputDevices) { |
| // Get the video stream for the current video input device |
| // to later use for the available video tracks. |
| final videoStream = await _getVideoStreamForDevice( |
| mediaDevices, |
| videoInputDevice.deviceId!, |
| ); |
| |
| // Get all video tracks in the video stream |
| // to later extract the lens direction from the first track. |
| final videoTracks = videoStream.getVideoTracks(); |
| |
| if (videoTracks.isNotEmpty) { |
| // Get the facing mode from the first available video track. |
| final facingMode = _cameraSettings.getFacingModeForVideoTrack( |
| videoTracks.first, |
| ); |
| |
| // Get the lens direction based on the facing mode. |
| // Fallback to the external lens direction |
| // if the facing mode is not available. |
| final lensDirection = facingMode != null |
| ? _cameraSettings.mapFacingModeToLensDirection(facingMode) |
| : CameraLensDirection.external; |
| |
| // Create a camera description. |
| // |
| // The name is a camera label which might be empty |
| // if no permissions to media devices have been granted. |
| // |
| // MediaDeviceInfo.label: |
| // https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/label |
| // |
| // Sensor orientation is currently not supported. |
| final cameraLabel = videoInputDevice.label ?? ''; |
| final camera = CameraDescription( |
| name: cameraLabel, |
| lensDirection: lensDirection, |
| sensorOrientation: 0, |
| ); |
| |
| final cameraMetadata = CameraMetadata( |
| deviceId: videoInputDevice.deviceId!, |
| facingMode: facingMode, |
| ); |
| |
| cameras.add(camera); |
| |
| camerasMetadata[camera] = cameraMetadata; |
| } else { |
| // Ignore as no video tracks exist in the current video input device. |
| continue; |
| } |
| } |
| |
| return cameras; |
| } |
| |
| @override |
| Future<int> createCamera( |
| CameraDescription cameraDescription, |
| ResolutionPreset? resolutionPreset, { |
| bool enableAudio = false, |
| }) async { |
| if (!camerasMetadata.containsKey(cameraDescription)) { |
| throw CameraException( |
| CameraErrorCodes.missingMetadata, |
| 'Missing camera metadata. Make sure to call `availableCameras` before creating a camera.', |
| ); |
| } |
| |
| final textureId = _textureCounter++; |
| |
| final cameraMetadata = camerasMetadata[cameraDescription]!; |
| |
| final cameraType = cameraMetadata.facingMode != null |
| ? _cameraSettings.mapFacingModeToCameraType(cameraMetadata.facingMode!) |
| : null; |
| |
| // Use the highest resolution possible |
| // if the resolution preset is not specified. |
| final videoSize = _cameraSettings |
| .mapResolutionPresetToSize(resolutionPreset ?? ResolutionPreset.max); |
| |
| // Create a camera with the given audio and video constraints. |
| // Sensor orientation is currently not supported. |
| final camera = Camera( |
| textureId: textureId, |
| window: window, |
| options: CameraOptions( |
| audio: AudioConstraints(enabled: enableAudio), |
| video: VideoConstraints( |
| facingMode: |
| cameraType != null ? FacingModeConstraint(cameraType) : null, |
| width: VideoSizeConstraint( |
| ideal: videoSize.width.toInt(), |
| ), |
| height: VideoSizeConstraint( |
| ideal: videoSize.height.toInt(), |
| ), |
| deviceId: cameraMetadata.deviceId, |
| ), |
| ), |
| ); |
| |
| cameras[textureId] = camera; |
| |
| return textureId; |
| } |
| |
| @override |
| Future<void> initializeCamera( |
| int cameraId, { |
| // The image format group is currently not supported. |
| ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, |
| }) async { |
| final camera = getCamera(cameraId); |
| |
| await camera.initialize(); |
| await camera.play(); |
| |
| final cameraSize = await camera.getVideoSize(); |
| |
| cameraEventStreamController.add( |
| CameraInitializedEvent( |
| cameraId, |
| cameraSize.width, |
| cameraSize.height, |
| // TODO(camera_web): Add support for exposure mode and point (https://github.com/flutter/flutter/issues/86857). |
| ExposureMode.auto, |
| false, |
| // TODO(camera_web): Add support for focus mode and point (https://github.com/flutter/flutter/issues/86858). |
| FocusMode.auto, |
| false, |
| ), |
| ); |
| } |
| |
| @override |
| Stream<CameraInitializedEvent> onCameraInitialized(int cameraId) { |
| return _cameraEvents(cameraId).whereType<CameraInitializedEvent>(); |
| } |
| |
| @override |
| Stream<CameraResolutionChangedEvent> onCameraResolutionChanged(int cameraId) { |
| throw UnimplementedError('onCameraResolutionChanged() is not implemented.'); |
| } |
| |
| @override |
| Stream<CameraClosingEvent> onCameraClosing(int cameraId) { |
| throw UnimplementedError('onCameraClosing() is not implemented.'); |
| } |
| |
| @override |
| Stream<CameraErrorEvent> onCameraError(int cameraId) { |
| throw UnimplementedError('onCameraError() is not implemented.'); |
| } |
| |
| @override |
| Stream<VideoRecordedEvent> onVideoRecordedEvent(int cameraId) { |
| throw UnimplementedError('onVideoRecordedEvent() is not implemented.'); |
| } |
| |
| @override |
| Stream<DeviceOrientationChangedEvent> onDeviceOrientationChanged() { |
| throw UnimplementedError( |
| 'onDeviceOrientationChanged() is not implemented.', |
| ); |
| } |
| |
| @override |
| Future<void> lockCaptureOrientation( |
| int cameraId, |
| DeviceOrientation orientation, |
| ) { |
| throw UnimplementedError('lockCaptureOrientation() is not implemented.'); |
| } |
| |
| @override |
| Future<void> unlockCaptureOrientation(int cameraId) { |
| throw UnimplementedError('unlockCaptureOrientation() is not implemented.'); |
| } |
| |
| @override |
| Future<XFile> takePicture(int cameraId) { |
| return getCamera(cameraId).takePicture(); |
| } |
| |
| @override |
| Future<void> prepareForVideoRecording() { |
| throw UnimplementedError('prepareForVideoRecording() is not implemented.'); |
| } |
| |
| @override |
| Future<void> startVideoRecording(int cameraId, {Duration? maxVideoDuration}) { |
| throw UnimplementedError('startVideoRecording() is not implemented.'); |
| } |
| |
| @override |
| Future<XFile> stopVideoRecording(int cameraId) { |
| throw UnimplementedError('stopVideoRecording() is not implemented.'); |
| } |
| |
| @override |
| Future<void> pauseVideoRecording(int cameraId) { |
| throw UnimplementedError('pauseVideoRecording() is not implemented.'); |
| } |
| |
| @override |
| Future<void> resumeVideoRecording(int cameraId) { |
| throw UnimplementedError('resumeVideoRecording() is not implemented.'); |
| } |
| |
| @override |
| Future<void> setFlashMode(int cameraId, FlashMode mode) { |
| throw UnimplementedError('setFlashMode() is not implemented.'); |
| } |
| |
| @override |
| Future<void> setExposureMode(int cameraId, ExposureMode mode) { |
| throw UnimplementedError('setExposureMode() is not implemented.'); |
| } |
| |
| @override |
| Future<void> setExposurePoint(int cameraId, Point<double>? point) { |
| throw UnimplementedError('setExposurePoint() is not implemented.'); |
| } |
| |
| @override |
| Future<double> getMinExposureOffset(int cameraId) { |
| throw UnimplementedError('getMinExposureOffset() is not implemented.'); |
| } |
| |
| @override |
| Future<double> getMaxExposureOffset(int cameraId) { |
| throw UnimplementedError('getMaxExposureOffset() is not implemented.'); |
| } |
| |
| @override |
| Future<double> getExposureOffsetStepSize(int cameraId) { |
| throw UnimplementedError('getExposureOffsetStepSize() is not implemented.'); |
| } |
| |
| @override |
| Future<double> setExposureOffset(int cameraId, double offset) { |
| throw UnimplementedError('setExposureOffset() is not implemented.'); |
| } |
| |
| @override |
| Future<void> setFocusMode(int cameraId, FocusMode mode) { |
| throw UnimplementedError('setFocusMode() is not implemented.'); |
| } |
| |
| @override |
| Future<void> setFocusPoint(int cameraId, Point<double>? point) { |
| throw UnimplementedError('setFocusPoint() is not implemented.'); |
| } |
| |
| @override |
| Future<double> getMaxZoomLevel(int cameraId) { |
| throw UnimplementedError('getMaxZoomLevel() is not implemented.'); |
| } |
| |
| @override |
| Future<double> getMinZoomLevel(int cameraId) { |
| throw UnimplementedError('getMinZoomLevel() is not implemented.'); |
| } |
| |
| @override |
| Future<void> setZoomLevel(int cameraId, double zoom) { |
| throw UnimplementedError('setZoomLevel() is not implemented.'); |
| } |
| |
| @override |
| Widget buildPreview(int cameraId) { |
| return HtmlElementView( |
| viewType: getCamera(cameraId).getViewType(), |
| ); |
| } |
| |
| @override |
| Future<void> dispose(int cameraId) { |
| throw UnimplementedError('dispose() is not implemented.'); |
| } |
| |
| /// Returns a media video stream for the device with the given [deviceId]. |
| Future<html.MediaStream> _getVideoStreamForDevice( |
| html.MediaDevices mediaDevices, |
| String deviceId, |
| ) { |
| // Create camera options with the desired device id. |
| final cameraOptions = CameraOptions( |
| video: VideoConstraints(deviceId: deviceId), |
| ); |
| |
| return mediaDevices.getUserMedia(cameraOptions.toJson()); |
| } |
| |
| /// Returns a camera for the given [cameraId]. |
| /// |
| /// Throws a [CameraException] if the camera does not exist. |
| @visibleForTesting |
| Camera getCamera(int cameraId) { |
| final camera = cameras[cameraId]; |
| |
| if (camera == null) { |
| throw CameraException( |
| CameraErrorCodes.notFound, |
| 'No camera found for the given camera id $cameraId.', |
| ); |
| } |
| |
| return camera; |
| } |
| } |