[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';
 }