From 3b8f4218d9860f2cdccd34ae1dea2fd6ba5309bc Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 22 Jul 2021 11:56:54 +0200 Subject: [PATCH 1/7] feat: add getVideoSize to Camera --- .../camera/camera_web/lib/src/camera.dart | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 41692d548882..334f117be274 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:html' as html; +import 'dart:ui'; import 'shims/dart_ui.dart' as ui; import 'package:camera_platform_interface/camera_platform_interface.dart'; @@ -171,6 +172,30 @@ class Camera { return XFile(html.Url.createObjectUrl(blob)); } + /// Returns a size of the camera video based on its first video track size. + /// + /// Returns [Size.zero] if the camera is missing a video track or + /// the video track does not include the width or height setting. + Future getVideoSize() async { + final videoTracks = videoElement.srcObject?.getVideoTracks() ?? []; + + if (videoTracks.isEmpty) { + return Size.zero; + } + + final defaultVideoTrack = videoTracks.first; + final defaultVideoTrackSettings = defaultVideoTrack.getSettings(); + + final width = defaultVideoTrackSettings['width']; + final height = defaultVideoTrackSettings['height']; + + if (width != null && height != null) { + return Size(width, height); + } else { + return Size.zero; + } + } + /// Disposes the camera by stopping the camera stream /// and reloading the camera source. void dispose() { From bd6683f3eb938c7f4a51f4571d1391c41d39d516 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 22 Jul 2021 11:58:19 +0200 Subject: [PATCH 2/7] test: add getVIdeoSize tests --- .../example/integration_test/camera_test.dart | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 0f1dcf7049d9..649c5f54a512 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_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.dart'; @@ -469,6 +470,49 @@ void main() { }); }); + group('getVideoSize', () { + testWidgets( + 'returns a size ' + 'based on the first video track settings', (tester) async { + const videoSize = Size(1280, 720); + + final videoElement = getVideoElementWithBlankStream(videoSize); + mediaStream = videoElement.captureStream(); + + final camera = Camera( + textureId: 1, + window: window, + ); + + await camera.initialize(); + + expect( + await camera.getVideoSize(), + equals(videoSize), + ); + }); + + testWidgets( + 'returns Size.zero ' + 'if the camera is missing video tracks', (tester) async { + // Create a video stream with no video tracks. + final videoElement = VideoElement(); + mediaStream = videoElement.captureStream(); + + final camera = Camera( + textureId: 1, + window: window, + ); + + await camera.initialize(); + + expect( + await camera.getVideoSize(), + equals(Size.zero), + ); + }); + }); + group('dispose', () { testWidgets('resets the video element\'s source', (tester) async { final camera = Camera( From c2efbe8b36a0a12bf4a772f7abfdfc4500e8eebf Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 22 Jul 2021 11:59:23 +0200 Subject: [PATCH 3/7] feat: add initializeCamera and onCameraInitialized implementation --- .../camera/camera_web/lib/src/camera_web.dart | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 80ab13d37d13..e58572e50ee4 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -13,6 +13,7 @@ import 'package:camera_web/src/types/types.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:stream_transform/stream_transform.dart'; /// The web implementation of [CameraPlatform]. /// @@ -42,6 +43,18 @@ class CameraPlugin extends CameraPlatform { @visibleForTesting final camerasMetadata = {}; + /// The controller used to broadcast different camera events. + /// + /// It is `broadcast` as multiple controllers may subscribe + /// to different stream views of this controller. + @visibleForTesting + final cameraEventStreamController = StreamController.broadcast(); + + /// Returns a stream of camera events for the given [cameraId]. + Stream _cameraEvents(int cameraId) => + cameraEventStreamController.stream + .where((event) => event.cameraId == cameraId); + /// The current browser window used to access media devices. @visibleForTesting html.Window? window = html.window; @@ -186,14 +199,34 @@ class CameraPlugin extends CameraPlatform { @override Future initializeCamera( int cameraId, { + // The image format group is currently not supported. ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, - }) { - throw UnimplementedError('initializeCamera() is not implemented.'); + }) async { + final camera = getCamera(cameraId); + + await camera.initialize(); + await camera.play(); + + final cameraSize = await camera.getVideoSize(); + + cameraEventStreamController.add( + CameraInitializedEvent( + cameraId, + cameraSize.width, + cameraSize.height, + // TODO(camera_web): Add support for exposure mode and point (https://github.com/flutter/flutter/issues/86857). + ExposureMode.auto, + false, + // TODO(camera_web): Add support for focus mode and point (https://github.com/flutter/flutter/issues/86858). + FocusMode.auto, + false, + ), + ); } @override Stream onCameraInitialized(int cameraId) { - throw UnimplementedError('onCameraInitialized() is not implemented.'); + return _cameraEvents(cameraId).whereType(); } @override @@ -348,4 +381,21 @@ class CameraPlugin extends CameraPlatform { return mediaDevices.getUserMedia(cameraOptions.toJson()); } + + /// Returns a camera for the given [cameraId]. + /// + /// Throws a [CameraException] if the camera does not exist. + @visibleForTesting + Camera getCamera(int cameraId) { + final camera = cameras[cameraId]; + + if (camera == null) { + throw CameraException( + CameraErrorCodes.notFound, + 'No camera found for the given camera id $cameraId.', + ); + } + + return camera; + } } From a1f200d52b2491b1797fe515e0688ba4d177866e Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 22 Jul 2021 12:01:21 +0200 Subject: [PATCH 4/7] test: add initializeCamera and onCameraInitialized tests --- .../integration_test/camera_web_test.dart | 134 +++++++++++++++--- .../integration_test/helpers/mocks.dart | 3 + 2 files changed, 115 insertions(+), 22 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 eef17ecfdff9..7681d1a98a07 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 @@ -5,6 +5,7 @@ import 'dart:html'; import 'dart:ui'; +import 'package:async/async.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/camera_web.dart'; import 'package:camera_web/src/camera.dart'; @@ -327,21 +328,18 @@ void main() { 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, - ); + final cameraDescription = CameraDescription( + name: 'name', + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ); - cameraMetadata = CameraMetadata( - deviceId: 'deviceId', - facingMode: 'user', - ); + final cameraMetadata = CameraMetadata( + deviceId: 'deviceId', + facingMode: 'user', + ); + setUp(() { // Add metadata for the camera description. (CameraPlatform.instance as CameraPlugin) .camerasMetadata[cameraDescription] = cameraMetadata; @@ -434,11 +432,38 @@ void main() { }); }); - testWidgets('initializeCamera throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.initializeCamera(cameraId), - throwsUnimplementedError, - ); + group('initializeCamera', () { + testWidgets( + 'throws CameraException ' + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.initializeCamera(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notFound, + ), + ), + ); + }); + + testWidgets('initializes and plays the camera', (tester) async { + final camera = MockCamera(); + + when(camera.getVideoSize).thenAnswer((_) => Future.value(Size(10, 10))); + when(camera.initialize).thenAnswer((_) => Future.value()); + when(camera.play).thenAnswer((_) => Future.value()); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.initializeCamera(cameraId); + + verify(camera.initialize).called(1); + verify(camera.play).called(1); + }); }); testWidgets('lockCaptureOrientation throws UnimplementedError', @@ -628,13 +653,78 @@ void main() { ); }); + group('getCamera', () { + testWidgets('returns the correct camera', (tester) async { + final camera = Camera(textureId: cameraId, window: window); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + (CameraPlatform.instance as CameraPlugin).getCamera(cameraId), + equals(camera), + ); + }); + + testWidgets( + 'throws CameraException ' + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => (CameraPlatform.instance as CameraPlugin).getCamera(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCodes.notFound, + ), + ), + ); + }); + }); + group('events', () { - testWidgets('onCameraInitialized throws UnimplementedError', - (tester) async { + testWidgets( + 'onCameraInitialized emits a CameraInitializedEvent ' + 'on initializeCamera', (tester) async { + // Mock the camera to use a blank video stream of size 1280x720. + const videoSize = Size(1280, 720); + + videoElement = getVideoElementWithBlankStream(videoSize); + + when( + () => mediaDevices.getUserMedia(any()), + ).thenAnswer((_) async => videoElement.captureStream()); + + final camera = Camera( + textureId: cameraId, + window: window, + ); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final Stream eventStream = + CameraPlatform.instance.onCameraInitialized(cameraId); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + expect( - () => CameraPlatform.instance.onCameraInitialized(cameraId), - throwsUnimplementedError, + await streamQueue.next, + CameraInitializedEvent( + cameraId, + videoSize.width, + videoSize.height, + ExposureMode.auto, + false, + FocusMode.auto, + false, + ), ); + + await streamQueue.cancel(); }); testWidgets('onCameraResolutionChanged throws UnimplementedError', 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 3702aee8e184..678c771d827f 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.dart'; import 'package:camera_web/src/camera_settings.dart'; import 'package:mocktail/mocktail.dart'; @@ -17,6 +18,8 @@ class MockCameraSettings extends Mock implements CameraSettings {} class MockMediaStreamTrack extends Mock implements MediaStreamTrack {} +class MockCamera extends Mock implements Camera {} + /// A fake [MediaStream] that returns the provided [_videoTracks]. class FakeMediaStream extends Fake implements MediaStream { FakeMediaStream(this._videoTracks); From 2abbba19df164877eb2c48955236a674a7d9b666 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 22 Jul 2021 12:06:51 +0200 Subject: [PATCH 5/7] test: use a blank canvas stream instead of an HTTPS stream --- .../example/integration_test/camera_test.dart | 8 +------- .../integration_test/camera_web_test.dart | 9 ++------- .../integration_test/helpers/mocks.dart | 20 +++++++++++++++++++ 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 649c5f54a512..6eeed23ecf56 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -29,13 +29,7 @@ void main() { navigator = MockNavigator(); mediaDevices = MockMediaDevices(); - final videoElement = VideoElement() - ..src = - 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4' - ..preload = 'true' - ..width = 10 - ..height = 10; - + final videoElement = getVideoElementWithBlankStream(Size(10, 10)); mediaStream = videoElement.captureStream(); when(() => window.navigator).thenReturn(navigator); 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 7681d1a98a07..d5e1835391ad 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 @@ -34,13 +34,8 @@ void main() { window = MockWindow(); navigator = MockNavigator(); mediaDevices = MockMediaDevices(); - videoElement = VideoElement() - ..src = - 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4' - ..preload = 'true' - ..width = 10 - ..height = 10 - ..crossOrigin = 'anonymous'; + + videoElement = getVideoElementWithBlankStream(Size(10, 10)); cameraSettings = MockCameraSettings(); 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 678c771d827f..fa627ca0b7e6 100644 --- a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:html'; +import 'dart:ui'; import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/camera_settings.dart'; @@ -57,3 +58,22 @@ class FakeDomException extends Fake implements DomException { @override String get name => _name; } + +/// Returns a video element with a blank stream of size [videoSize]. +/// +/// Can be used to mock a video stream: +/// ```dart +/// final videoElement = getVideoElementWithBlankStream(Size(100, 100)); +/// final videoStream = videoElement.captureStream(); +/// ``` +VideoElement getVideoElementWithBlankStream(Size videoSize) { + final canvasElement = CanvasElement( + width: videoSize.width.toInt(), + height: videoSize.height.toInt(), + )..context2D.fillRect(0, 0, videoSize.width, videoSize.height); + + final videoElement = VideoElement() + ..srcObject = canvasElement.captureStream(); + + return videoElement; +} From b8b8c8b93999f49110b06b7cbe7f025a6a5d1cbe Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 22 Jul 2021 14:05:30 +0200 Subject: [PATCH 6/7] docs: update comments --- .../lib/src/platform_interface/camera_platform.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart index 4437d3b0593a..0211d1f09977 100644 --- a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -64,6 +64,7 @@ abstract class CameraPlatform extends PlatformInterface { /// [imageFormatGroup] is used to specify the image formatting used. /// On Android this defaults to ImageFormat.YUV_420_888 and applies only to the imageStream. /// On iOS this defaults to kCVPixelFormatType_32BGRA. + /// On Web this parameter is currently not supported. Future initializeCamera( int cameraId, { ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, From 55fa4fdcb46c4b5eb14bdba55653717c98210ddb Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 23 Jul 2021 16:26:54 -0700 Subject: [PATCH 7/7] Revert "docs: update comments" This reverts commit b8b8c8b93999f49110b06b7cbe7f025a6a5d1cbe. --- .../lib/src/platform_interface/camera_platform.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart index 0211d1f09977..4437d3b0593a 100644 --- a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -64,7 +64,6 @@ abstract class CameraPlatform extends PlatformInterface { /// [imageFormatGroup] is used to specify the image formatting used. /// On Android this defaults to ImageFormat.YUV_420_888 and applies only to the imageStream. /// On iOS this defaults to kCVPixelFormatType_32BGRA. - /// On Web this parameter is currently not supported. Future initializeCamera( int cameraId, { ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown,