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 new file mode 100644 index 000000000000..c1c00fe7a337 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart @@ -0,0 +1,210 @@ +// 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'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraSettings', () { + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + late CameraSettings settings; + + setUp(() async { + window = MockWindow(); + navigator = MockNavigator(); + mediaDevices = MockMediaDevices(); + + when(() => window.navigator).thenReturn(navigator); + when(() => navigator.mediaDevices).thenReturn(mediaDevices); + + settings = CameraSettings()..window = window; + }); + + group('getFacingModeForVideoTrack', () { + testWidgets( + 'throws CameraException ' + 'with notSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + expect( + () => settings.getFacingModeForVideoTrack(MockMediaStreamTrack()), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notSupported, + ), + ), + ); + }); + + testWidgets( + 'returns null ' + 'when the facing mode is not supported', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'facingMode': false, + }); + + final facingMode = + settings.getFacingModeForVideoTrack(MockMediaStreamTrack()); + + expect( + facingMode, + equals(null), + ); + }); + + group('when the facing mode is supported', () { + setUp(() { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'facingMode': true, + }); + }); + + testWidgets( + 'returns an appropriate facing mode ' + 'based on the video track settings', (tester) async { + final videoTrack = MockMediaStreamTrack(); + + when(videoTrack.getSettings).thenReturn({'facingMode': 'user'}); + + final facingMode = settings.getFacingModeForVideoTrack(videoTrack); + + expect( + facingMode, + equals('user'), + ); + }); + + testWidgets( + 'returns an appropriate facing mode ' + 'based on the video track capabilities ' + 'when the facing mode setting is empty', (tester) async { + final videoTrack = MockMediaStreamTrack(); + + when(videoTrack.getSettings).thenReturn({}); + when(videoTrack.getCapabilities).thenReturn({ + 'facingMode': ['environment', 'left'] + }); + + final facingMode = settings.getFacingModeForVideoTrack(videoTrack); + + expect( + facingMode, + equals('environment'), + ); + }); + + testWidgets( + 'returns null ' + 'when the facing mode setting ' + 'and capabilities are empty', (tester) async { + final videoTrack = MockMediaStreamTrack(); + + when(videoTrack.getSettings).thenReturn({}); + when(videoTrack.getCapabilities).thenReturn({'facingMode': []}); + + final facingMode = settings.getFacingModeForVideoTrack(videoTrack); + + expect( + facingMode, + equals(null), + ); + }); + + testWidgets( + 'returns null ' + 'when the facing mode setting is empty and ' + 'the video track capabilities are not supported', (tester) async { + final videoTrack = MockMediaStreamTrack(); + + when(videoTrack.getSettings).thenReturn({}); + when(videoTrack.getCapabilities).thenThrow(JSNoSuchMethodError()); + + final facingMode = settings.getFacingModeForVideoTrack(videoTrack); + + expect( + facingMode, + equals(null), + ); + }); + + testWidgets( + 'throws CameraException ' + 'with unknown error ' + 'when getting the video track capabilities ' + 'throws an unknown error', (tester) async { + final videoTrack = MockMediaStreamTrack(); + + when(videoTrack.getSettings).thenReturn({}); + when(videoTrack.getCapabilities).thenThrow(Exception('Unknown')); + + expect( + () => settings.getFacingModeForVideoTrack(videoTrack), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.unknown, + ), + ), + ); + }); + }); + }); + + group('mapFacingModeToLensDirection', () { + testWidgets( + 'returns front ' + 'when the facing mode is user', (tester) async { + expect( + settings.mapFacingModeToLensDirection('user'), + equals(CameraLensDirection.front), + ); + }); + + testWidgets( + 'returns back ' + 'when the facing mode is environment', (tester) async { + expect( + settings.mapFacingModeToLensDirection('environment'), + equals(CameraLensDirection.back), + ); + }); + + testWidgets( + 'returns external ' + 'when the facing mode is left', (tester) async { + expect( + settings.mapFacingModeToLensDirection('left'), + equals(CameraLensDirection.external), + ); + }); + + testWidgets( + 'returns external ' + 'when the facing mode is right', (tester) async { + expect( + settings.mapFacingModeToLensDirection('right'), + equals(CameraLensDirection.external), + ); + }); + }); + }); +} + +class JSNoSuchMethodError implements Exception {} 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 d26f0e855889..25368daf02f7 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 @@ -6,6 +6,8 @@ import 'dart:html'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/camera_web.dart'; +import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/types/types.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -23,6 +25,7 @@ void main() { late Navigator navigator; late MediaDevices mediaDevices; late VideoElement videoElement; + late CameraSettings cameraSettings; setUp(() async { window = MockWindow(); @@ -33,7 +36,10 @@ void main() { 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4' ..preload = 'true' ..width = 10 - ..height = 10; + ..height = 10 + ..crossOrigin = 'anonymous'; + + cameraSettings = MockCameraSettings(); when(() => window.navigator).thenReturn(navigator); when(() => navigator.mediaDevices).thenReturn(mediaDevices); @@ -41,18 +47,253 @@ void main() { () => mediaDevices.getUserMedia(any()), ).thenAnswer((_) async => videoElement.captureStream()); - CameraPlatform.instance = CameraPlugin()..window = window; + CameraPlatform.instance = CameraPlugin( + cameraSettings: cameraSettings, + )..window = window; + }); + + setUpAll(() { + registerFallbackValue(MockMediaStreamTrack()); }); testWidgets('CameraPlugin is the live instance', (tester) async { expect(CameraPlatform.instance, isA()); }); - testWidgets('availableCameras throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.availableCameras(), - throwsUnimplementedError, - ); + group('availableCameras', () { + setUp(() { + when( + () => cameraSettings.getFacingModeForVideoTrack( + any(), + ), + ).thenReturn(null); + }); + + testWidgets( + 'throws CameraException ' + 'with notSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + expect( + () => CameraPlatform.instance.availableCameras(), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notSupported, + ), + ), + ); + }); + + testWidgets( + 'calls MediaDevices.getUserMedia ' + 'on the video input device', (tester) async { + final videoDevice = FakeMediaDeviceInfo( + '1', + 'Camera 1', + MediaDeviceKind.videoInput, + ); + + when(mediaDevices.enumerateDevices).thenAnswer( + (_) => Future.value([videoDevice]), + ); + + final _ = await CameraPlatform.instance.availableCameras(); + + verify( + () => mediaDevices.getUserMedia( + CameraOptions( + video: VideoConstraints( + deviceId: videoDevice.deviceId, + ), + ).toJson(), + ), + ).called(1); + }); + + testWidgets( + 'calls CameraSettings.getLensDirectionForVideoTrack ' + 'on the first video track of the video input device', (tester) async { + final videoDevice = FakeMediaDeviceInfo( + '1', + 'Camera 1', + MediaDeviceKind.videoInput, + ); + + final videoStream = + FakeMediaStream([MockMediaStreamTrack(), MockMediaStreamTrack()]); + + when( + () => mediaDevices.getUserMedia( + CameraOptions( + video: VideoConstraints(deviceId: videoDevice.deviceId), + ).toJson(), + ), + ).thenAnswer((_) => Future.value(videoStream)); + + when(mediaDevices.enumerateDevices).thenAnswer( + (_) => Future.value([videoDevice]), + ); + + final _ = await CameraPlatform.instance.availableCameras(); + + verify( + () => cameraSettings.getFacingModeForVideoTrack( + videoStream.getVideoTracks().first, + ), + ).called(1); + }); + + testWidgets( + 'returns appropriate camera descriptions ' + 'for multiple media devices', (tester) async { + final firstVideoDevice = FakeMediaDeviceInfo( + '1', + 'Camera 1', + MediaDeviceKind.videoInput, + ); + + final secondVideoDevice = FakeMediaDeviceInfo( + '4', + 'Camera 4', + MediaDeviceKind.videoInput, + ); + + // Create a video stream for the first video device. + final firstVideoStream = + FakeMediaStream([MockMediaStreamTrack(), MockMediaStreamTrack()]); + + // Create a video stream for the second video device. + final secondVideoStream = FakeMediaStream([MockMediaStreamTrack()]); + + // Mock media devices to return two video input devices + // and two audio devices. + when(mediaDevices.enumerateDevices).thenAnswer( + (_) => Future.value([ + firstVideoDevice, + FakeMediaDeviceInfo( + '2', + 'Camera 2', + MediaDeviceKind.audioInput, + ), + FakeMediaDeviceInfo( + '3', + 'Camera 3', + MediaDeviceKind.audioOutput, + ), + secondVideoDevice, + ]), + ); + + // Mock media devices to return the first video stream + // for the first video device. + when( + () => mediaDevices.getUserMedia( + CameraOptions( + video: VideoConstraints(deviceId: firstVideoDevice.deviceId), + ).toJson(), + ), + ).thenAnswer((_) => Future.value(firstVideoStream)); + + // Mock media devices to return the second video stream + // for the second video device. + when( + () => mediaDevices.getUserMedia( + CameraOptions( + video: VideoConstraints(deviceId: secondVideoDevice.deviceId), + ).toJson(), + ), + ).thenAnswer((_) => Future.value(secondVideoStream)); + + // Mock camera settings to return a user facing mode + // for the first video stream. + when( + () => cameraSettings.getFacingModeForVideoTrack( + firstVideoStream.getVideoTracks().first, + ), + ).thenReturn('user'); + + when(() => cameraSettings.mapFacingModeToLensDirection('user')) + .thenReturn(CameraLensDirection.front); + + // Mock camera settings to return an environment facing mode + // for the second video stream. + when( + () => cameraSettings.getFacingModeForVideoTrack( + secondVideoStream.getVideoTracks().first, + ), + ).thenReturn('environment'); + + when(() => cameraSettings.mapFacingModeToLensDirection('environment')) + .thenReturn(CameraLensDirection.back); + + final cameras = await CameraPlatform.instance.availableCameras(); + + // Expect two cameras and ignore two audio devices. + expect( + cameras, + equals([ + CameraDescription( + name: firstVideoDevice.label!, + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ), + CameraDescription( + name: secondVideoDevice.label!, + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ) + ]), + ); + }); + + testWidgets( + 'sets camera metadata ' + 'for the camera description', (tester) async { + final videoDevice = FakeMediaDeviceInfo( + '1', + 'Camera 1', + MediaDeviceKind.videoInput, + ); + + final videoStream = + FakeMediaStream([MockMediaStreamTrack(), MockMediaStreamTrack()]); + + when(mediaDevices.enumerateDevices).thenAnswer( + (_) => Future.value([videoDevice]), + ); + + when( + () => mediaDevices.getUserMedia( + CameraOptions( + video: VideoConstraints(deviceId: videoDevice.deviceId), + ).toJson(), + ), + ).thenAnswer((_) => Future.value(videoStream)); + + when( + () => cameraSettings.getFacingModeForVideoTrack( + videoStream.getVideoTracks().first, + ), + ).thenReturn('left'); + + when(() => cameraSettings.mapFacingModeToLensDirection('left')) + .thenReturn(CameraLensDirection.external); + + final camera = (await CameraPlatform.instance.availableCameras()).first; + + expect( + (CameraPlatform.instance as CameraPlugin).camerasMetadata, + equals({ + camera: CameraMetadata( + deviceId: videoDevice.deviceId!, + facingMode: 'left', + ) + }), + ); + }); }); testWidgets('createCamera throws UnimplementedError', (tester) async { diff --git a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart index 03be3f0b3ca6..3702aee8e184 100644 --- a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -4,6 +4,7 @@ import 'dart:html'; +import 'package:camera_web/src/camera_settings.dart'; import 'package:mocktail/mocktail.dart'; class MockWindow extends Mock implements Window {} @@ -12,12 +13,44 @@ class MockNavigator extends Mock implements Navigator {} class MockMediaDevices extends Mock implements MediaDevices {} -/// A fake [DomException] that returns the provided [errorName]. +class MockCameraSettings extends Mock implements CameraSettings {} + +class MockMediaStreamTrack extends Mock implements MediaStreamTrack {} + +/// A fake [MediaStream] that returns the provided [_videoTracks]. +class FakeMediaStream extends Fake implements MediaStream { + FakeMediaStream(this._videoTracks); + + final List _videoTracks; + + @override + List getVideoTracks() => _videoTracks; +} + +/// A fake [MediaDeviceInfo] that returns the provided [_deviceId], [_label] and [_kind]. +class FakeMediaDeviceInfo extends Fake implements MediaDeviceInfo { + FakeMediaDeviceInfo(this._deviceId, this._label, this._kind); + + final String _deviceId; + final String _label; + final String _kind; + + @override + String? get deviceId => _deviceId; + + @override + String? get label => _label; + + @override + String? get kind => _kind; +} + +/// A fake [DomException] that returns the provided error [_name]. class FakeDomException extends Fake implements DomException { - FakeDomException(this.errorName); + FakeDomException(this._name); - final String errorName; + final String _name; @override - String get name => errorName; + String get name => _name; } diff --git a/packages/camera/camera_web/lib/src/camera_settings.dart b/packages/camera/camera_web/lib/src/camera_settings.dart new file mode 100644 index 000000000000..2a1a31ff1cf5 --- /dev/null +++ b/packages/camera/camera_web/lib/src/camera_settings.dart @@ -0,0 +1,108 @@ +// 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 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/foundation.dart'; + +/// A utility to fetch and map camera settings. +class CameraSettings { + // A facing mode constraint name. + static const _facingModeKey = "facingMode"; + + /// The current browser window used to access media devices. + @visibleForTesting + html.Window? window = html.window; + + /// 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 CameraException( + CameraErrorCodes.notSupported, + '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) { + try { + // 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 + // + // This may throw a not supported error on Firefox. + final videoTrackCapabilities = videoTrack.getCapabilities(); + + // A list of facing mode capabilities as + // the camera may support multiple facing modes. + final facingModeCapabilities = + List.from(videoTrackCapabilities[_facingModeKey] ?? []); + + if (facingModeCapabilities.isNotEmpty) { + final facingModeCapability = facingModeCapabilities.first; + return facingModeCapability; + } else { + // Return null if there are no facing mode capabilities. + return null; + } + } catch (e) { + switch (e.runtimeType.toString()) { + case 'JSNoSuchMethodError': + // Return null if getting capabilities is currently not supported. + return null; + default: + throw CameraException( + CameraErrorCodes.unknown, + 'An unknown error occured when getting the video track capabilities.', + ); + } + } + } + + 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; + } + } +} diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index fc3be09eec1d..ae9937dd94d3 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -7,6 +7,8 @@ import 'dart:html' as html; import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.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'; @@ -15,18 +17,112 @@ import 'package:flutter_web_plugins/flutter_web_plugins.dart'; /// /// 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(); + CameraPlatform.instance = CameraPlugin( + cameraSettings: CameraSettings(), + ); } - /// The current browser window used to access device cameras. + final CameraSettings _cameraSettings; + + /// Metadata associated with each camera description. + /// Populated in [availableCameras]. + @visibleForTesting + final camerasMetadata = {}; + + /// The current browser window used to access media devices. @visibleForTesting - html.Window? window; + html.Window? window = html.window; @override - Future> availableCameras() { - throw UnimplementedError('availableCameras() is not implemented.'); + Future> availableCameras() async { + final mediaDevices = window?.navigator.mediaDevices; + final cameras = []; + + // 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() + .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 @@ -190,4 +286,17 @@ class CameraPlugin extends CameraPlatform { Future dispose(int cameraId) { throw UnimplementedError('dispose() is not implemented.'); } + + /// Returns a media video stream for the device with the given [deviceId]. + Future _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()); + } } diff --git a/packages/camera/camera_web/lib/src/types/camera_metadata.dart b/packages/camera/camera_web/lib/src/types/camera_metadata.dart new file mode 100644 index 000000000000..c9998e58a52c --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/camera_metadata.dart @@ -0,0 +1,37 @@ +// 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:ui' show hashValues; + +/// Metadata used along the camera description +/// to store additional web-specific camera details. +class CameraMetadata { + /// Creates a new instance of [CameraMetadata] + /// with the given [deviceId] and [facingMode]. + const CameraMetadata({required this.deviceId, required this.facingMode}); + + /// Uniquely identifies the camera device. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/deviceId + final String deviceId; + + /// Describes the direction the camera is facing towards. + /// May be `user`, `environment`, `left`, `right` + /// or null if the facing mode is not available. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings/facingMode + final String? facingMode; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CameraMetadata && + other.deviceId == deviceId && + other.facingMode == facingMode; + } + + @override + int get hashCode => hashValues(deviceId.hashCode, facingMode.hashCode); +} diff --git a/packages/camera/camera_web/lib/src/types/media_device_kind.dart b/packages/camera/camera_web/lib/src/types/media_device_kind.dart new file mode 100644 index 000000000000..1f746808df9e --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/media_device_kind.dart @@ -0,0 +1,17 @@ +// 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. + +/// A kind of a media device. +/// +/// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/kind +abstract class MediaDeviceKind { + /// A video input media device kind. + static const videoInput = 'videoinput'; + + /// An audio input media device kind. + static const audioInput = 'audioinput'; + + /// An audio output media device kind. + static const audioOutput = 'audiooutput'; +} diff --git a/packages/camera/camera_web/lib/src/types/types.dart b/packages/camera/camera_web/lib/src/types/types.dart index fc1f931679ff..1a15503715cd 100644 --- a/packages/camera/camera_web/lib/src/types/types.dart +++ b/packages/camera/camera_web/lib/src/types/types.dart @@ -3,4 +3,6 @@ // found in the LICENSE file. export 'camera_error_codes.dart'; +export 'camera_metadata.dart'; export 'camera_options.dart'; +export 'media_device_kind.dart'; diff --git a/packages/camera/camera_web/test/types/camera_metadata_test.dart b/packages/camera/camera_web/test/types/camera_metadata_test.dart new file mode 100644 index 000000000000..c76688f768d7 --- /dev/null +++ b/packages/camera/camera_web/test/types/camera_metadata_test.dart @@ -0,0 +1,25 @@ +// 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 'package:camera_web/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('CameraMetadata', () { + test('supports value equality', () { + expect( + CameraMetadata( + deviceId: 'deviceId', + facingMode: 'environment', + ), + equals( + CameraMetadata( + deviceId: 'deviceId', + facingMode: 'environment', + ), + ), + ); + }); + }); +}