[camera_web] Add `createCamera` implementation (#4182)
diff --git a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart
index c1c00fe..ddfb86e 100644
--- a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart
+++ b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:html';
+import 'dart:ui';
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:camera_web/src/camera_settings.dart';
@@ -204,6 +205,100 @@
);
});
});
+
+ group('mapFacingModeToCameraType', () {
+ testWidgets(
+ 'returns user '
+ 'when the facing mode is user', (tester) async {
+ expect(
+ settings.mapFacingModeToCameraType('user'),
+ equals(CameraType.user),
+ );
+ });
+
+ testWidgets(
+ 'returns environment '
+ 'when the facing mode is environment', (tester) async {
+ expect(
+ settings.mapFacingModeToCameraType('environment'),
+ equals(CameraType.environment),
+ );
+ });
+
+ testWidgets(
+ 'returns user '
+ 'when the facing mode is left', (tester) async {
+ expect(
+ settings.mapFacingModeToCameraType('left'),
+ equals(CameraType.user),
+ );
+ });
+
+ testWidgets(
+ 'returns user '
+ 'when the facing mode is right', (tester) async {
+ expect(
+ settings.mapFacingModeToCameraType('right'),
+ equals(CameraType.user),
+ );
+ });
+ });
+
+ group('mapResolutionPresetToSize', () {
+ testWidgets(
+ 'returns 3840x2160 '
+ 'when the resolution preset is max', (tester) async {
+ expect(
+ settings.mapResolutionPresetToSize(ResolutionPreset.max),
+ equals(Size(3840, 2160)),
+ );
+ });
+
+ testWidgets(
+ 'returns 3840x2160 '
+ 'when the resolution preset is ultraHigh', (tester) async {
+ expect(
+ settings.mapResolutionPresetToSize(ResolutionPreset.ultraHigh),
+ equals(Size(3840, 2160)),
+ );
+ });
+
+ testWidgets(
+ 'returns 1920x1080 '
+ 'when the resolution preset is veryHigh', (tester) async {
+ expect(
+ settings.mapResolutionPresetToSize(ResolutionPreset.veryHigh),
+ equals(Size(1920, 1080)),
+ );
+ });
+
+ testWidgets(
+ 'returns 1280x720 '
+ 'when the resolution preset is high', (tester) async {
+ expect(
+ settings.mapResolutionPresetToSize(ResolutionPreset.high),
+ equals(Size(1280, 720)),
+ );
+ });
+
+ testWidgets(
+ 'returns 720x480 '
+ 'when the resolution preset is medium', (tester) async {
+ expect(
+ settings.mapResolutionPresetToSize(ResolutionPreset.medium),
+ equals(Size(720, 480)),
+ );
+ });
+
+ testWidgets(
+ 'returns 320x240 '
+ 'when the resolution preset is low', (tester) async {
+ expect(
+ settings.mapResolutionPresetToSize(ResolutionPreset.low),
+ equals(Size(320, 240)),
+ );
+ });
+ });
});
}
diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart
index 25368da..eef17ec 100644
--- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart
+++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart
@@ -3,9 +3,11 @@
// found in the LICENSE file.
import 'dart:html';
+import 'dart:ui';
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:camera_web/camera_web.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/services.dart';
@@ -296,18 +298,140 @@
});
});
- testWidgets('createCamera throws UnimplementedError', (tester) async {
- expect(
- () => CameraPlatform.instance.createCamera(
- CameraDescription(
- name: 'name',
- lensDirection: CameraLensDirection.external,
- sensorOrientation: 0,
+ group('createCamera', () {
+ testWidgets(
+ 'throws CameraException '
+ 'with missingMetadata error '
+ 'if there is no metadata '
+ 'for the given camera description', (tester) async {
+ expect(
+ () => CameraPlatform.instance.createCamera(
+ CameraDescription(
+ name: 'name',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 0,
+ ),
+ ResolutionPreset.ultraHigh,
),
- ResolutionPreset.medium,
- ),
- throwsUnimplementedError,
- );
+ throwsA(
+ isA<CameraException>().having(
+ (e) => e.code,
+ 'code',
+ CameraErrorCodes.missingMetadata,
+ ),
+ ),
+ );
+ });
+
+ group('creates a camera', () {
+ const ultraHighResolutionSize = Size(3840, 2160);
+ const maxResolutionSize = Size(3840, 2160);
+
+ late CameraDescription cameraDescription;
+ late CameraMetadata cameraMetadata;
+
+ setUp(() {
+ cameraDescription = CameraDescription(
+ name: 'name',
+ lensDirection: CameraLensDirection.front,
+ sensorOrientation: 0,
+ );
+
+ cameraMetadata = CameraMetadata(
+ deviceId: 'deviceId',
+ facingMode: 'user',
+ );
+
+ // Add metadata for the camera description.
+ (CameraPlatform.instance as CameraPlugin)
+ .camerasMetadata[cameraDescription] = cameraMetadata;
+
+ when(
+ () => cameraSettings.mapFacingModeToCameraType('user'),
+ ).thenReturn(CameraType.user);
+ });
+
+ testWidgets('with appropriate options', (tester) async {
+ when(
+ () => cameraSettings
+ .mapResolutionPresetToSize(ResolutionPreset.ultraHigh),
+ ).thenReturn(ultraHighResolutionSize);
+
+ final cameraId = await CameraPlatform.instance.createCamera(
+ cameraDescription,
+ ResolutionPreset.ultraHigh,
+ enableAudio: true,
+ );
+
+ expect(
+ (CameraPlatform.instance as CameraPlugin).cameras[cameraId],
+ isA<Camera>()
+ .having(
+ (camera) => camera.textureId,
+ 'textureId',
+ cameraId,
+ )
+ .having(
+ (camera) => camera.window,
+ 'window',
+ window,
+ )
+ .having(
+ (camera) => camera.options,
+ 'options',
+ CameraOptions(
+ audio: AudioConstraints(enabled: true),
+ video: VideoConstraints(
+ facingMode: FacingModeConstraint(CameraType.user),
+ width: VideoSizeConstraint(
+ ideal: ultraHighResolutionSize.width.toInt(),
+ ),
+ height: VideoSizeConstraint(
+ ideal: ultraHighResolutionSize.height.toInt(),
+ ),
+ deviceId: cameraMetadata.deviceId,
+ ),
+ ),
+ ),
+ );
+ });
+
+ testWidgets(
+ 'with a max resolution preset '
+ 'and enabled audio set to false '
+ 'when no options are specified', (tester) async {
+ when(
+ () =>
+ cameraSettings.mapResolutionPresetToSize(ResolutionPreset.max),
+ ).thenReturn(maxResolutionSize);
+
+ final cameraId = await CameraPlatform.instance.createCamera(
+ cameraDescription,
+ null,
+ );
+
+ expect(
+ (CameraPlatform.instance as CameraPlugin).cameras[cameraId],
+ isA<Camera>().having(
+ (camera) => camera.options,
+ 'options',
+ CameraOptions(
+ audio: AudioConstraints(enabled: false),
+ video: VideoConstraints(
+ facingMode: FacingModeConstraint(CameraType.user),
+ width: VideoSizeConstraint(
+ ideal: maxResolutionSize.width.toInt(),
+ ),
+ height: VideoSizeConstraint(
+ ideal: maxResolutionSize.height.toInt(),
+ ),
+ deviceId: cameraMetadata.deviceId,
+ ),
+ ),
+ ),
+ );
+ });
+ });
});
testWidgets('initializeCamera throws UnimplementedError', (tester) async {
diff --git a/packages/camera/camera_web/lib/src/camera_settings.dart b/packages/camera/camera_web/lib/src/camera_settings.dart
index 2a1a31f..7b87840 100644
--- a/packages/camera/camera_web/lib/src/camera_settings.dart
+++ b/packages/camera/camera_web/lib/src/camera_settings.dart
@@ -3,6 +3,7 @@
// 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/types/types.dart';
@@ -105,4 +106,38 @@
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(3840, 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);
+ }
+ }
}
diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart
index ae9937d..80ab13d 100644
--- a/packages/camera/camera_web/lib/src/camera_web.dart
+++ b/packages/camera/camera_web/lib/src/camera_web.dart
@@ -7,6 +7,7 @@
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';
@@ -31,6 +32,11 @@
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
@@ -130,8 +136,51 @@
CameraDescription cameraDescription,
ResolutionPreset? resolutionPreset, {
bool enableAudio = false,
- }) {
- throw UnimplementedError('createCamera() is not implemented.');
+ }) 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
diff --git a/packages/camera/camera_web/lib/src/types/camera_error_codes.dart b/packages/camera/camera_web/lib/src/types/camera_error_codes.dart
index f8dc5df..afb02ae 100644
--- a/packages/camera/camera_web/lib/src/types/camera_error_codes.dart
+++ b/packages/camera/camera_web/lib/src/types/camera_error_codes.dart
@@ -24,6 +24,9 @@
/// to access the media input from an insecure context.
static const type = 'cameraType';
+ /// The camera metadata is missing.
+ static const missingMetadata = 'missingMetadata';
+
/// An unknown camera error.
static const unknown = 'cameraUnknown';
}