From 479a204a15860d6dc578fc6128228fed9e610033 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 16 Jul 2021 12:32:17 +0200 Subject: [PATCH 01/15] feat: add media device kind --- .../camera_web/lib/src/types/media_device_kind.dart | 12 ++++++++++++ packages/camera/camera_web/lib/src/types/types.dart | 1 + 2 files changed, 13 insertions(+) create mode 100644 packages/camera/camera_web/lib/src/types/media_device_kind.dart 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..d3e031842ce6 --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/media_device_kind.dart @@ -0,0 +1,12 @@ +/// A kind of a media device. +/// 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..2533f14899d2 100644 --- a/packages/camera/camera_web/lib/src/types/types.dart +++ b/packages/camera/camera_web/lib/src/types/types.dart @@ -4,3 +4,4 @@ export 'camera_error_codes.dart'; export 'camera_options.dart'; +export 'media_device_kind.dart'; From afb94d392d3a2a7b27a551517d3687dce3bbbcf8 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 16 Jul 2021 12:32:50 +0200 Subject: [PATCH 02/15] feat: add camera settings --- .../camera_web/lib/src/camera_settings.dart | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 packages/camera/camera_web/lib/src/camera_settings.dart 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..148aeaaefaf3 --- /dev/null +++ b/packages/camera/camera_web/lib/src/camera_settings.dart @@ -0,0 +1,94 @@ +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 camera settings +/// based on the camera tracks. +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 camera lens direction based on the [videoTrack]'s facing mode. + CameraLensDirection getLensDirectionForVideoTrack( + 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 facing mode is supported by the current browser. + final supportedConstraints = mediaDevices.getSupportedConstraints(); + final facingModeSupported = supportedConstraints[_facingModeKey] ?? false; + + // Fallback to the external lens direction + // if the facing mode is not supported. + if (!facingModeSupported) { + return CameraLensDirection.external; + } + + // 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 on the video track settings, + // check for the facing mode in video track capabilities. + // + // MediaTrackCapabilities: + // https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackcapabilities + 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 mapFacingModeToLensDirection(facingModeCapability); + } else { + // Fallback to the external lens direction + // if there are no facing mode capabilities. + return CameraLensDirection.external; + } + } + + return mapFacingModeToLensDirection(facingMode); + } + + /// Maps the facing mode to appropriate camera lens direction. + /// + /// The following values for the facing mode are supported: + /// https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings/facingMode + @visibleForTesting + CameraLensDirection mapFacingModeToLensDirection(String facingMode) { + switch (facingMode) { + case 'user': + return CameraLensDirection.front; + case 'environment': + return CameraLensDirection.back; + case 'left': + case 'right': + default: + return CameraLensDirection.external; + } + } +} From 1e3e8081add3efa1a2608eef589267946133611a Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 16 Jul 2021 12:37:38 +0200 Subject: [PATCH 03/15] test: add camera settings tests --- .../camera_settings_test.dart | 172 ++++++++++++++++++ .../integration_test/helpers/mocks.dart | 10 +- 2 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 packages/camera/camera_web/example/integration_test/camera_settings_test.dart 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..7d83da2d5ab1 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart @@ -0,0 +1,172 @@ +// 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('getLensDirectionForVideoTrack', () { + testWidgets( + 'throws CameraException ' + 'with notSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + expect( + () => settings.getLensDirectionForVideoTrack(MockMediaStreamTrack()), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notSupported, + ), + ), + ); + }); + + testWidgets( + 'returns external ' + 'when the facing mode is not supported', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'facingMode': false, + }); + + final lensDirection = + settings.getLensDirectionForVideoTrack(MockMediaStreamTrack()); + + expect( + lensDirection, + equals(CameraLensDirection.external), + ); + }); + + group('when the facing mode is supported', () { + setUp(() { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'facingMode': true, + }); + }); + + testWidgets( + 'returns appropriate lens direction ' + 'based on the video track settings', (tester) async { + final videoTrack = MockMediaStreamTrack(); + + when(videoTrack.getSettings).thenReturn({'facingMode': 'user'}); + + final lensDirection = + settings.getLensDirectionForVideoTrack(videoTrack); + + expect( + lensDirection, + equals(CameraLensDirection.front), + ); + }); + + testWidgets( + 'returns appropriate lens direction ' + '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 lensDirection = + settings.getLensDirectionForVideoTrack(videoTrack); + + expect( + lensDirection, + equals(CameraLensDirection.back), + ); + }); + + testWidgets( + 'returns external ' + 'when the facing mode setting ' + 'and capabilities are empty', (tester) async { + final videoTrack = MockMediaStreamTrack(); + + when(videoTrack.getSettings).thenReturn({}); + when(videoTrack.getCapabilities).thenReturn({'facingMode': []}); + + final lensDirection = + settings.getLensDirectionForVideoTrack(videoTrack); + + expect( + lensDirection, + equals(CameraLensDirection.external), + ); + }); + }); + }); + + group('mapFacingModeToLensDirection', () { + testWidgets( + 'returns front ' + 'when the facing mode is user', (tester) async { + expect( + settings.mapFacingModeToLensDirection('user'), + CameraLensDirection.front, + ); + }); + + testWidgets( + 'returns back ' + 'when the facing mode is environment', (tester) async { + expect( + settings.mapFacingModeToLensDirection('environment'), + CameraLensDirection.back, + ); + }); + + testWidgets( + 'returns external ' + 'when the facing mode is left', (tester) async { + expect( + settings.mapFacingModeToLensDirection('left'), + CameraLensDirection.external, + ); + }); + + testWidgets( + 'returns external ' + 'when the facing mode is right', (tester) async { + expect( + settings.mapFacingModeToLensDirection('right'), + CameraLensDirection.external, + ); + }); + }); + }); +} 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..c2241d1f931e 100644 --- a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -12,12 +12,14 @@ class MockNavigator extends Mock implements Navigator {} class MockMediaDevices extends Mock implements MediaDevices {} -/// A fake [DomException] that returns the provided [errorName]. +class MockMediaStreamTrack extends Mock implements MediaStreamTrack {} + +/// 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; } From f692ccee34932ff9a4c424574aa83b1d46fd0e1a Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 16 Jul 2021 12:38:24 +0200 Subject: [PATCH 04/15] feat: add availableCameras implementation --- .../camera/camera_web/lib/src/camera_web.dart | 101 +++++++++++++++++- 1 file changed, 96 insertions(+), 5 deletions(-) diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index fc3be09eec1d..98b6fcad1c72 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,94 @@ 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; + + /// 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 mode from the first track. + final videoTracks = videoStream.getVideoTracks(); + + if (videoTracks.isNotEmpty) { + // Get the lens direction from the first available video track. + final lensDirection = _cameraSettings.getLensDirectionForVideoTrack( + videoTracks.first, + ); + + // Create a camera description. + // + // The name is a camera label with a fallback to a device id if empty. + // This is because the label might not exist if no permissions 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.isNotEmpty ? cameraLabel : videoInputDevice.deviceId!, + lensDirection: lensDirection, + sensorOrientation: 0, + ); + + cameras.add(camera); + } else { + // Ignore as no video tracks exist in the current video input device. + continue; + } + } + + return cameras; } @override @@ -190,4 +268,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()); + } } From ea0a31a9e84bda865f5de91498aa82361c139c1e Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 16 Jul 2021 12:38:37 +0200 Subject: [PATCH 05/15] test: add availableCameras tests --- .../integration_test/camera_web_test.dart | 205 +++++++++++++++++- .../integration_test/helpers/mocks.dart | 31 +++ 2 files changed, 229 insertions(+), 7 deletions(-) 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..437689dd85dd 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,203 @@ 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.getLensDirectionForVideoTrack( + any(), + ), + ).thenReturn(CameraLensDirection.external); + }); + + 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.getLensDirectionForVideoTrack( + 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', + '', // The device label might be empty. + 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 front lens direction + // for the first video stream. + when( + () => cameraSettings.getLensDirectionForVideoTrack( + firstVideoStream.getVideoTracks().first, + ), + ).thenReturn(CameraLensDirection.front); + + // Mock camera settings to return a back lens direction + // for the second video stream. + when( + () => cameraSettings.getLensDirectionForVideoTrack( + secondVideoStream.getVideoTracks().first, + ), + ).thenReturn(CameraLensDirection.back); + + final cameras = await CameraPlatform.instance.availableCameras(); + + // Expect two cameras and ignore two audio devices. + expect( + cameras, + equals([ + CameraDescription( + // Uses the device label as a camera name. + name: firstVideoDevice.label!, + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ), + CameraDescription( + // Fallbacks to the device id if the label is empty. + name: secondVideoDevice.deviceId!, + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ) + ]), + ); + }); }); 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 c2241d1f931e..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,8 +13,38 @@ class MockNavigator extends Mock implements Navigator {} class MockMediaDevices extends Mock implements MediaDevices {} +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._name); From 380e7fb02cbfe239da0aa05b20902241a99388de Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 16 Jul 2021 12:38:51 +0200 Subject: [PATCH 06/15] feat: add camera_web to camera example temporarily --- packages/camera/camera/example/pubspec.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml index eb8995e2f354..7ad53da07291 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -14,6 +14,13 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ + + # Temporarily include the web implementation of a camera plugin + # as it is not yet integrated into the official package. + # TODO(bselwe): Remove when camera_web is integrated into the camera package. + camera_web: + path: ../../camera_web/ + path_provider: ^2.0.0 flutter: sdk: flutter From 6f62a8eea279924e68f9e982e7538c5331e19462 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Mon, 19 Jul 2021 12:11:18 +0200 Subject: [PATCH 07/15] feat: add cameras metadata --- .../camera/camera_web/lib/src/camera_web.dart | 13 ++++++++++++- .../lib/src/types/camera_metadata.dart | 19 +++++++++++++++++++ .../camera_web/lib/src/types/types.dart | 1 + 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 packages/camera/camera_web/lib/src/types/camera_metadata.dart diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 98b6fcad1c72..d0e37152f168 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -31,6 +31,10 @@ class CameraPlugin extends CameraPlatform { final CameraSettings _cameraSettings; + /// Metadata associated with each camera description. + /// Populated in [availableCameras]. + final _camerasMetadata = {}; + /// The current browser window used to access media devices. @visibleForTesting html.Window? window = html.window; @@ -57,7 +61,7 @@ class CameraPlugin extends CameraPlatform { .whereType() .where((device) => device.kind == MediaDeviceKind.videoInput) - /// The device id property is currently not supported on Internet Explorer. + /// 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); @@ -97,7 +101,14 @@ class CameraPlugin extends CameraPlatform { 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; 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..99fdd3e91bc0 --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/camera_metadata.dart @@ -0,0 +1,19 @@ +/// 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; +} diff --git a/packages/camera/camera_web/lib/src/types/types.dart b/packages/camera/camera_web/lib/src/types/types.dart index 2533f14899d2..1a15503715cd 100644 --- a/packages/camera/camera_web/lib/src/types/types.dart +++ b/packages/camera/camera_web/lib/src/types/types.dart @@ -3,5 +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'; From 2277edb90a7ba42248558518589a7d8ff17d2a6a Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Mon, 19 Jul 2021 12:12:56 +0200 Subject: [PATCH 08/15] feat: update getLensDirection to getFacingMode for the video track --- .../camera_settings_test.dart | 49 +++++++++---------- .../integration_test/camera_web_test.dart | 30 +++++++----- .../camera_web/lib/src/camera_settings.dart | 31 +++++------- .../camera/camera_web/lib/src/camera_web.dart | 18 ++++--- .../lib/src/types/media_device_kind.dart | 3 +- 5 files changed, 67 insertions(+), 64 deletions(-) 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 7d83da2d5ab1..3819701370b0 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 @@ -33,7 +33,7 @@ void main() { settings = CameraSettings()..window = window; }); - group('getLensDirectionForVideoTrack', () { + group('getFacingModeForVideoTrack', () { testWidgets( 'throws CameraException ' 'with notSupported error ' @@ -41,7 +41,7 @@ void main() { when(() => navigator.mediaDevices).thenReturn(null); expect( - () => settings.getLensDirectionForVideoTrack(MockMediaStreamTrack()), + () => settings.getFacingModeForVideoTrack(MockMediaStreamTrack()), throwsA( isA().having( (e) => e.code, @@ -53,18 +53,18 @@ void main() { }); testWidgets( - 'returns external ' + 'returns null ' 'when the facing mode is not supported', (tester) async { when(mediaDevices.getSupportedConstraints).thenReturn({ 'facingMode': false, }); - final lensDirection = - settings.getLensDirectionForVideoTrack(MockMediaStreamTrack()); + final facingMode = + settings.getFacingModeForVideoTrack(MockMediaStreamTrack()); expect( - lensDirection, - equals(CameraLensDirection.external), + facingMode, + equals(null), ); }); @@ -76,23 +76,22 @@ void main() { }); testWidgets( - 'returns appropriate lens direction ' + 'returns an appropriate facing mode ' 'based on the video track settings', (tester) async { final videoTrack = MockMediaStreamTrack(); when(videoTrack.getSettings).thenReturn({'facingMode': 'user'}); - final lensDirection = - settings.getLensDirectionForVideoTrack(videoTrack); + final facingMode = settings.getFacingModeForVideoTrack(videoTrack); expect( - lensDirection, - equals(CameraLensDirection.front), + facingMode, + equals('user'), ); }); testWidgets( - 'returns appropriate lens direction ' + 'returns an appropriate facing mode ' 'based on the video track capabilities ' 'when the facing mode setting is empty', (tester) async { final videoTrack = MockMediaStreamTrack(); @@ -102,17 +101,16 @@ void main() { 'facingMode': ['environment', 'left'] }); - final lensDirection = - settings.getLensDirectionForVideoTrack(videoTrack); + final facingMode = settings.getFacingModeForVideoTrack(videoTrack); expect( - lensDirection, - equals(CameraLensDirection.back), + facingMode, + equals('environment'), ); }); testWidgets( - 'returns external ' + 'returns null ' 'when the facing mode setting ' 'and capabilities are empty', (tester) async { final videoTrack = MockMediaStreamTrack(); @@ -120,12 +118,11 @@ void main() { when(videoTrack.getSettings).thenReturn({}); when(videoTrack.getCapabilities).thenReturn({'facingMode': []}); - final lensDirection = - settings.getLensDirectionForVideoTrack(videoTrack); + final facingMode = settings.getFacingModeForVideoTrack(videoTrack); expect( - lensDirection, - equals(CameraLensDirection.external), + facingMode, + equals(null), ); }); }); @@ -137,7 +134,7 @@ void main() { 'when the facing mode is user', (tester) async { expect( settings.mapFacingModeToLensDirection('user'), - CameraLensDirection.front, + equals(CameraLensDirection.front), ); }); @@ -146,7 +143,7 @@ void main() { 'when the facing mode is environment', (tester) async { expect( settings.mapFacingModeToLensDirection('environment'), - CameraLensDirection.back, + equals(CameraLensDirection.back), ); }); @@ -155,7 +152,7 @@ void main() { 'when the facing mode is left', (tester) async { expect( settings.mapFacingModeToLensDirection('left'), - CameraLensDirection.external, + equals(CameraLensDirection.external), ); }); @@ -164,7 +161,7 @@ void main() { 'when the facing mode is right', (tester) async { expect( settings.mapFacingModeToLensDirection('right'), - CameraLensDirection.external, + equals(CameraLensDirection.external), ); }); }); 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 437689dd85dd..cae3230f39af 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 @@ -63,10 +63,10 @@ void main() { group('availableCameras', () { setUp(() { when( - () => cameraSettings.getLensDirectionForVideoTrack( + () => cameraSettings.getFacingModeForVideoTrack( any(), ), - ).thenReturn(CameraLensDirection.external); + ).thenReturn(null); }); testWidgets( @@ -140,7 +140,7 @@ void main() { final _ = await CameraPlatform.instance.availableCameras(); verify( - () => cameraSettings.getLensDirectionForVideoTrack( + () => cameraSettings.getFacingModeForVideoTrack( videoStream.getVideoTracks().first, ), ).called(1); @@ -157,7 +157,7 @@ void main() { final secondVideoDevice = FakeMediaDeviceInfo( '4', - '', // The device label might be empty. + 'Camera 4', MediaDeviceKind.videoInput, ); @@ -207,21 +207,27 @@ void main() { ), ).thenAnswer((_) => Future.value(secondVideoStream)); - // Mock camera settings to return a front lens direction + // Mock camera settings to return a user facing mode // for the first video stream. when( - () => cameraSettings.getLensDirectionForVideoTrack( + () => cameraSettings.getFacingModeForVideoTrack( firstVideoStream.getVideoTracks().first, ), - ).thenReturn(CameraLensDirection.front); + ).thenReturn('user'); - // Mock camera settings to return a back lens direction + when(() => cameraSettings.mapFacingModeToLensDirection('user')) + .thenReturn(CameraLensDirection.front); + + // Mock camera settings to return an environment facing mode // for the second video stream. when( - () => cameraSettings.getLensDirectionForVideoTrack( + () => cameraSettings.getFacingModeForVideoTrack( secondVideoStream.getVideoTracks().first, ), - ).thenReturn(CameraLensDirection.back); + ).thenReturn('environment'); + + when(() => cameraSettings.mapFacingModeToLensDirection('environment')) + .thenReturn(CameraLensDirection.back); final cameras = await CameraPlatform.instance.availableCameras(); @@ -230,14 +236,12 @@ void main() { cameras, equals([ CameraDescription( - // Uses the device label as a camera name. name: firstVideoDevice.label!, lensDirection: CameraLensDirection.front, sensorOrientation: 0, ), CameraDescription( - // Fallbacks to the device id if the label is empty. - name: secondVideoDevice.deviceId!, + name: secondVideoDevice.label!, lensDirection: CameraLensDirection.back, sensorOrientation: 0, ) diff --git a/packages/camera/camera_web/lib/src/camera_settings.dart b/packages/camera/camera_web/lib/src/camera_settings.dart index 148aeaaefaf3..c85cb0927a75 100644 --- a/packages/camera/camera_web/lib/src/camera_settings.dart +++ b/packages/camera/camera_web/lib/src/camera_settings.dart @@ -4,8 +4,7 @@ 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 camera settings -/// based on the camera tracks. +/// A utility to fetch and map camera settings. class CameraSettings { // A facing mode constraint name. static const _facingModeKey = "facingMode"; @@ -14,10 +13,9 @@ class CameraSettings { @visibleForTesting html.Window? window = html.window; - /// Returns a camera lens direction based on the [videoTrack]'s facing mode. - CameraLensDirection getLensDirectionForVideoTrack( - html.MediaStreamTrack videoTrack, - ) { + /// 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 @@ -29,14 +27,13 @@ class CameraSettings { ); } - // Check if the facing mode is supported by the current browser. + // Check if the camera facing mode is supported by the current browser. final supportedConstraints = mediaDevices.getSupportedConstraints(); final facingModeSupported = supportedConstraints[_facingModeKey] ?? false; - // Fallback to the external lens direction - // if the facing mode is not supported. + // Return null if the facing mode is not supported. if (!facingModeSupported) { - return CameraLensDirection.external; + return null; } // Extract the facing mode from the video track settings. @@ -49,7 +46,7 @@ class CameraSettings { final facingMode = videoTrackSettings[_facingModeKey]; if (facingMode == null) { - // If the facing mode does not exist on the video track settings, + // If the facing mode does not exist in the video track settings, // check for the facing mode in video track capabilities. // // MediaTrackCapabilities: @@ -63,22 +60,20 @@ class CameraSettings { if (facingModeCapabilities.isNotEmpty) { final facingModeCapability = facingModeCapabilities.first; - return mapFacingModeToLensDirection(facingModeCapability); + return facingModeCapability; } else { - // Fallback to the external lens direction - // if there are no facing mode capabilities. - return CameraLensDirection.external; + // Return null if there are no facing mode capabilities. + return null; } } - return mapFacingModeToLensDirection(facingMode); + return facingMode; } - /// Maps the facing mode to appropriate camera lens direction. + /// 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 - @visibleForTesting CameraLensDirection mapFacingModeToLensDirection(String facingMode) { switch (facingMode) { case 'user': diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index d0e37152f168..9d06a44d661c 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -79,15 +79,22 @@ class CameraPlugin extends CameraPlatform { final videoTracks = videoStream.getVideoTracks(); if (videoTracks.isNotEmpty) { - // Get the lens direction from the first available video track. - final lensDirection = _cameraSettings.getLensDirectionForVideoTrack( + // 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 with a fallback to a device id if empty. - // This is because the label might not exist if no permissions have been granted. + // 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 @@ -95,8 +102,7 @@ class CameraPlugin extends CameraPlatform { // Sensor orientation is currently not supported. final cameraLabel = videoInputDevice.label ?? ''; final camera = CameraDescription( - name: - cameraLabel.isNotEmpty ? cameraLabel : videoInputDevice.deviceId!, + name: cameraLabel, lensDirection: lensDirection, sensorOrientation: 0, ); 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 index d3e031842ce6..ffed7ce1a338 100644 --- a/packages/camera/camera_web/lib/src/types/media_device_kind.dart +++ b/packages/camera/camera_web/lib/src/types/media_device_kind.dart @@ -1,5 +1,6 @@ /// A kind of a media device. -/// https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/kind +/// +/// 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'; From 1f1d541957b5bc64638201cd3cbc00cd0219e61d Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Mon, 19 Jul 2021 18:01:14 +0200 Subject: [PATCH 09/15] feat: add value equality for camera metadata --- .../types/camera_metadata_test.dart | 28 +++++++++++++++++++ .../lib/src/types/camera_metadata.dart | 18 ++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 packages/camera/camera_web/example/integration_test/types/camera_metadata_test.dart diff --git a/packages/camera/camera_web/example/integration_test/types/camera_metadata_test.dart b/packages/camera/camera_web/example/integration_test/types/camera_metadata_test.dart new file mode 100644 index 000000000000..36ecb3e47f31 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/types/camera_metadata_test.dart @@ -0,0 +1,28 @@ +// 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'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraMetadata', () { + testWidgets('supports value equality', (tester) async { + expect( + CameraMetadata( + deviceId: 'deviceId', + facingMode: 'environment', + ), + equals( + CameraMetadata( + deviceId: 'deviceId', + facingMode: 'environment', + ), + ), + ); + }); + }); +} diff --git a/packages/camera/camera_web/lib/src/types/camera_metadata.dart b/packages/camera/camera_web/lib/src/types/camera_metadata.dart index 99fdd3e91bc0..c9998e58a52c 100644 --- a/packages/camera/camera_web/lib/src/types/camera_metadata.dart +++ b/packages/camera/camera_web/lib/src/types/camera_metadata.dart @@ -1,3 +1,9 @@ +// 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 { @@ -16,4 +22,16 @@ class CameraMetadata { /// /// 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); } From 02cc7331e1099529cc7e2b2a07e347c6c52a98d4 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Mon, 19 Jul 2021 18:03:44 +0200 Subject: [PATCH 10/15] test: add availableCameras camera metadata tests --- .../integration_test/camera_web_test.dart | 46 +++++++++++++++++++ .../camera/camera_web/lib/src/camera_web.dart | 5 +- 2 files changed, 49 insertions(+), 2 deletions(-) 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 cae3230f39af..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 @@ -248,6 +248,52 @@ void main() { ]), ); }); + + 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/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 9d06a44d661c..b5f419800bee 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -33,7 +33,8 @@ class CameraPlugin extends CameraPlatform { /// Metadata associated with each camera description. /// Populated in [availableCameras]. - final _camerasMetadata = {}; + @visibleForTesting + final camerasMetadata = {}; /// The current browser window used to access media devices. @visibleForTesting @@ -114,7 +115,7 @@ class CameraPlugin extends CameraPlatform { cameras.add(camera); - _camerasMetadata[camera] = cameraMetadata; + camerasMetadata[camera] = cameraMetadata; } else { // Ignore as no video tracks exist in the current video input device. continue; From 9ab3212e482e9adbb826af29403f9f3a94c939b4 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Wed, 21 Jul 2021 11:45:42 +0200 Subject: [PATCH 11/15] feat: return null facing mode when getting the video track capabilities is not supported --- .../camera_settings_test.dart | 41 ++++++++++++++++ .../camera_web/lib/src/camera_settings.dart | 47 ++++++++++++------- 2 files changed, 72 insertions(+), 16 deletions(-) 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 3819701370b0..c1c00fe7a337 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 @@ -125,6 +125,45 @@ void main() { 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, + ), + ), + ); + }); }); }); @@ -167,3 +206,5 @@ void main() { }); }); } + +class JSNoSuchMethodError implements Exception {} diff --git a/packages/camera/camera_web/lib/src/camera_settings.dart b/packages/camera/camera_web/lib/src/camera_settings.dart index c85cb0927a75..487b60c50b96 100644 --- a/packages/camera/camera_web/lib/src/camera_settings.dart +++ b/packages/camera/camera_web/lib/src/camera_settings.dart @@ -46,24 +46,39 @@ class CameraSettings { 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 video track capabilities. - // - // MediaTrackCapabilities: - // https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackcapabilities - final videoTrackCapabilities = videoTrack.getCapabilities(); + 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] ?? []); + // 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; + 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.', + ); + } } } From 132dddd4dd0bee91764c196fb6f5631f8f95c758 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Wed, 21 Jul 2021 11:51:55 +0200 Subject: [PATCH 12/15] docs: add copyright --- packages/camera/camera/example/pubspec.yaml | 4 ++-- packages/camera/camera_web/lib/src/camera_settings.dart | 4 ++++ .../camera/camera_web/lib/src/types/media_device_kind.dart | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml index 7ad53da07291..fca082c0bfa8 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -15,9 +15,9 @@ dependencies: # the parent directory to use the current plugin's version. path: ../ - # Temporarily include the web implementation of a camera plugin + # Temporarily include a web implementation of the camera plugin # as it is not yet integrated into the official package. - # TODO(bselwe): Remove when camera_web is integrated into the camera package. + # TODO(bselwe): Remove when camera_web is integrated into the camera plugin. camera_web: path: ../../camera_web/ diff --git a/packages/camera/camera_web/lib/src/camera_settings.dart b/packages/camera/camera_web/lib/src/camera_settings.dart index 487b60c50b96..2a1a31ff1cf5 100644 --- a/packages/camera/camera_web/lib/src/camera_settings.dart +++ b/packages/camera/camera_web/lib/src/camera_settings.dart @@ -1,3 +1,7 @@ +// 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'; 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 index ffed7ce1a338..1f746808df9e 100644 --- a/packages/camera/camera_web/lib/src/types/media_device_kind.dart +++ b/packages/camera/camera_web/lib/src/types/media_device_kind.dart @@ -1,3 +1,7 @@ +// 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 From 05ce3df6870fab4c40b75d4c1239ceb8ec320bc5 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Wed, 21 Jul 2021 12:03:56 +0200 Subject: [PATCH 13/15] docs: update comments --- packages/camera/camera_web/lib/src/camera_web.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index b5f419800bee..ae9937dd94d3 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -76,7 +76,7 @@ class CameraPlugin extends CameraPlatform { ); // Get all video tracks in the video stream - // to later extract the lens direction mode from the first track. + // to later extract the lens direction from the first track. final videoTracks = videoStream.getVideoTracks(); if (videoTracks.isNotEmpty) { From 0e96878b2800073dcdda600b16a1011c7106f2b7 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 21 Jul 2021 16:28:51 -0700 Subject: [PATCH 14/15] Move camera metadata test to VM tests. --- .../types/camera_metadata_test.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) rename packages/camera/camera_web/{example/integration_test => test}/types/camera_metadata_test.dart (76%) diff --git a/packages/camera/camera_web/example/integration_test/types/camera_metadata_test.dart b/packages/camera/camera_web/test/types/camera_metadata_test.dart similarity index 76% rename from packages/camera/camera_web/example/integration_test/types/camera_metadata_test.dart rename to packages/camera/camera_web/test/types/camera_metadata_test.dart index 36ecb3e47f31..c76688f768d7 100644 --- a/packages/camera/camera_web/example/integration_test/types/camera_metadata_test.dart +++ b/packages/camera/camera_web/test/types/camera_metadata_test.dart @@ -4,13 +4,10 @@ import 'package:camera_web/src/types/types.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('CameraMetadata', () { - testWidgets('supports value equality', (tester) async { + test('supports value equality', () { expect( CameraMetadata( deviceId: 'deviceId', From 3577ebfa562f96e66a3cb39c783942059d246021 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 21 Jul 2021 16:38:28 -0700 Subject: [PATCH 15/15] Revert "feat: add camera_web to camera example temporarily" This reverts commit 380e7fb02cbfe239da0aa05b20902241a99388de. --- packages/camera/camera/example/pubspec.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml index fca082c0bfa8..eb8995e2f354 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -14,13 +14,6 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - - # Temporarily include a web implementation of the camera plugin - # as it is not yet integrated into the official package. - # TODO(bselwe): Remove when camera_web is integrated into the camera plugin. - camera_web: - path: ../../camera_web/ - path_provider: ^2.0.0 flutter: sdk: flutter