From 6fb3c2404aafc4fa45f6b4e51b9fcb45931a8d33 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 12 May 2022 15:02:07 -0400 Subject: [PATCH 01/10] Add platform interface versions of the image data classes --- .../lib/src/types/camera_image_data.dart | 119 ++++++++++++++++++ .../lib/src/types/types.dart | 1 + .../test/types/camera_image_data_test.dart | 38 ++++++ 3 files changed, 158 insertions(+) create mode 100644 packages/camera/camera_platform_interface/lib/src/types/camera_image_data.dart create mode 100644 packages/camera/camera_platform_interface/test/types/camera_image_data_test.dart diff --git a/packages/camera/camera_platform_interface/lib/src/types/camera_image_data.dart b/packages/camera/camera_platform_interface/lib/src/types/camera_image_data.dart new file mode 100644 index 000000000000..c7deab6cc495 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/camera_image_data.dart @@ -0,0 +1,119 @@ +// 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:typed_data'; + +import 'package:flutter/foundation.dart'; + +import '../../camera_platform_interface.dart'; + +/// A single color plane of image data. +/// +/// The number and meaning of the planes in an image are determined by its +/// format. +@immutable +class CameraImagePlane { + /// Creates a new instance with the given bytes and optional metadata. + const CameraImagePlane({ + required this.bytes, + required this.bytesPerRow, + this.bytesPerPixel, + this.height, + this.width, + }); + + /// Bytes representing this plane. + final Uint8List bytes; + + /// The row stride for this color plane, in bytes. + final int bytesPerRow; + + /// The distance between adjacent pixel samples in bytes, when available. + final int? bytesPerPixel; + + /// Height of the pixel buffer, when available + final int? height; + + /// Width of the pixel buffer, when available + final int? width; +} + +/// Describes how pixels are represented in an image. +@immutable +class CameraImageFormat { + /// Create a new format with the given cross-platform group and raw underyling + /// platform identifier. + const CameraImageFormat(this.group, {required this.raw}); + + /// Describes the format group the raw image format falls into. + final ImageFormatGroup group; + + /// Raw version of the format from the underlying platform. + /// + /// On Android, this should be an `int` from class + /// `android.graphics.ImageFormat`. See + /// https://developer.android.com/reference/android/graphics/ImageFormat + /// + /// On iOS, this should be a `FourCharCode` constant from Pixel Format + /// Identifiers. See + /// https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers + final dynamic raw; +} + +/// A single complete image buffer from the platform camera. +/// +/// This class allows for direct application access to the pixel data of an +/// Image through one or more [Uint8List]. Each buffer is encapsulated in a +/// [CameraImagePlane] that describes the layout of the pixel data in that +/// plane. [CameraImageData] is not directly usable as a UI resource. +/// +/// Although not all image formats are planar on all platforms, this class +/// treats 1-dimensional images as single planar images. +@immutable +class CameraImageData { + /// Creates a new instance with the given format, planes, and metadata. + const CameraImageData({ + required this.format, + required this.planes, + required this.height, + required this.width, + this.lensAperture, + this.sensorExposureTime, + this.sensorSensitivity, + }); + + /// Format of the image provided. + /// + /// Determines the number of planes needed to represent the image, and + /// the general layout of the pixel data in each [Uint8List]. + final CameraImageFormat format; + + /// Height of the image in pixels. + /// + /// For formats where some color channels are subsampled, this is the height + /// of the largest-resolution plane. + final int height; + + /// Width of the image in pixels. + /// + /// For formats where some color channels are subsampled, this is the width + /// of the largest-resolution plane. + final int width; + + /// The pixels planes for this image. + /// + /// The number of planes is determined by the format of the image. + final List planes; + + /// The aperture settings for this image. + /// + /// Represented as an f-stop value. + final double? lensAperture; + + /// The sensor exposure time for this image in nanoseconds. + final int? sensorExposureTime; + + /// The sensor sensitivity in standard ISO arithmetic units. + final double? sensorSensitivity; +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/types.dart b/packages/camera/camera_platform_interface/lib/src/types/types.dart index 0c24839d6445..3eb09fcb833c 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/types.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/types.dart @@ -4,6 +4,7 @@ export 'camera_description.dart'; export 'camera_exception.dart'; +export 'camera_image_data.dart'; export 'exposure_mode.dart'; export 'flash_mode.dart'; export 'focus_mode.dart'; diff --git a/packages/camera/camera_platform_interface/test/types/camera_image_data_test.dart b/packages/camera/camera_platform_interface/test/types/camera_image_data_test.dart new file mode 100644 index 000000000000..f06213e2b0e4 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/camera_image_data_test.dart @@ -0,0 +1,38 @@ +// 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:typed_data'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('CameraImageData can be created', () { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + final CameraImageData cameraImage = CameraImageData( + format: const CameraImageFormat(ImageFormatGroup.jpeg, raw: 42), + height: 100, + width: 200, + lensAperture: 1.8, + sensorExposureTime: 11, + sensorSensitivity: 92.0, + planes: [ + CameraImagePlane( + bytes: Uint8List.fromList([1, 2, 3, 4]), + bytesPerRow: 4, + bytesPerPixel: 2, + height: 100, + width: 200) + ], + ); + expect(cameraImage.format.group, ImageFormatGroup.jpeg); + expect(cameraImage.lensAperture, 1.8); + expect(cameraImage.sensorExposureTime, 11); + expect(cameraImage.sensorSensitivity, 92.0); + expect(cameraImage.height, 100); + expect(cameraImage.width, 200); + expect(cameraImage.planes.length, 1); + }); +} From d86e91202087e1460c04925fc4a088006ae654ec Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 13 May 2022 11:10:36 -0400 Subject: [PATCH 02/10] Platform interface package parts --- .../method_channel/method_channel_camera.dart | 53 ++++++++++++ .../src/method_channel/type_conversion.dart | 61 +++++++++++++ .../platform_interface/camera_platform.dart | 14 +++ .../method_channel_camera_test.dart | 46 ++++++++++ .../method_channel/type_conversion_test.dart | 85 +++++++++++++++++++ 5 files changed, 259 insertions(+) create mode 100644 packages/camera/camera_platform_interface/lib/src/method_channel/type_conversion.dart create mode 100644 packages/camera/camera_platform_interface/test/method_channel/type_conversion_test.dart diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart index c856f3467821..e2b9e4bfa505 100644 --- a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -12,6 +12,8 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_transform/stream_transform.dart'; +import 'type_conversion.dart'; + const MethodChannel _channel = MethodChannel('plugins.flutter.io/camera'); /// An implementation of [CameraPlatform] that uses method channels. @@ -48,6 +50,12 @@ class MethodChannelCamera extends CameraPlatform { final StreamController deviceEventStreamController = StreamController.broadcast(); + // The stream to receive frames from the native code. + StreamSubscription? _platformImageStreamSubscription; + + // The stream for vending frames to platform interface clients. + StreamController? _frameStreamController; + Stream _cameraEvents(int cameraId) => cameraEventStreamController.stream .where((CameraEvent event) => event.cameraId == cameraId); @@ -267,6 +275,51 @@ class MethodChannelCamera extends CameraPlatform { {'cameraId': cameraId}, ); + @override + Stream onStreamedFrameAvailable(int cameraId) { + _frameStreamController = StreamController( + onListen: _onFrameStreamListen, + onPause: _onFrameStreamPauseResume, + onResume: _onFrameStreamPauseResume, + onCancel: _onFrameStreamCancel, + ); + return _frameStreamController!.stream; + } + + void _onFrameStreamListen() { + _startPlatformStream(); + } + + Future _startPlatformStream() async { + await _channel.invokeMethod('startImageStream'); + const EventChannel cameraEventChannel = + EventChannel('plugins.flutter.io/camera/imageStream'); + _platformImageStreamSubscription = + cameraEventChannel.receiveBroadcastStream().listen((dynamic imageData) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + try { + _channel.invokeMethod('receivedImageStreamData'); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + _frameStreamController! + .add(cameraImageFromPlatformData(imageData as Map)); + }); + } + + FutureOr _onFrameStreamCancel() async { + await _channel.invokeMethod('stopImageStream'); + await _platformImageStreamSubscription?.cancel(); + _platformImageStreamSubscription = null; + _frameStreamController = null; + } + + void _onFrameStreamPauseResume() { + throw CameraException('InvalidCall', + 'Pause and resume are not supported for onStreamedFrameAvailable'); + } + @override Future setFlashMode(int cameraId, FlashMode mode) => _channel.invokeMethod( diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/type_conversion.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/type_conversion.dart new file mode 100644 index 000000000000..9dffbbf6ae3a --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/type_conversion.dart @@ -0,0 +1,61 @@ +// 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:typed_data'; + +import 'package:flutter/foundation.dart'; + +import '../types/types.dart'; + +/// Converts method channel call [data] for `receivedImageStreamData` to a +/// [CameraImageData]. +CameraImageData cameraImageFromPlatformData(Map data) { + return CameraImageData( + format: _cameraImageFormatFromPlatformData(data['format']), + height: data['height'] as int, + width: data['width'] as int, + lensAperture: data['lensAperture'] as double?, + sensorExposureTime: data['sensorExposureTime'] as int?, + sensorSensitivity: data['sensorSensitivity'] as double?, + planes: List.unmodifiable( + (data['planes'] as List).map( + (dynamic planeData) => _cameraImagePlaneFromPlatformData( + planeData as Map)))); +} + +CameraImageFormat _cameraImageFormatFromPlatformData(dynamic data) { + return CameraImageFormat(_imageFormatGroupFromPlatformData(data), raw: data); +} + +ImageFormatGroup _imageFormatGroupFromPlatformData(dynamic data) { + if (defaultTargetPlatform == TargetPlatform.android) { + switch (data) { + case 35: // android.graphics.ImageFormat.YUV_420_888 + return ImageFormatGroup.yuv420; + case 256: // android.graphics.ImageFormat.JPEG + return ImageFormatGroup.jpeg; + } + } + + if (defaultTargetPlatform == TargetPlatform.iOS) { + switch (data) { + case 875704438: // kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange + return ImageFormatGroup.yuv420; + + case 1111970369: // kCVPixelFormatType_32BGRA + return ImageFormatGroup.bgra8888; + } + } + + return ImageFormatGroup.unknown; +} + +CameraImagePlane _cameraImagePlaneFromPlatformData(Map data) { + return CameraImagePlane( + bytes: data['bytes'] as Uint8List, + bytesPerPixel: data['bytesPerPixel'] as int?, + bytesPerRow: data['bytesPerRow'] as int, + height: data['height'] as int?, + width: data['width'] as int?); +} 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 daa19b8b4011..e5946ec4a117 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 @@ -149,6 +149,20 @@ abstract class CameraPlatform extends PlatformInterface { throw UnimplementedError('resumeVideoRecording() is not implemented.'); } + /// A new streamed frame is available. + /// + /// Listening to this stream will start streaming, and canceling will stop. + /// Pausing will throw a [CameraException], as pausing the stream would cause + /// very high memory usage; to temporarily stop receiving frames, cancel, then + /// listen again later. + /// + /// + // TODO(bmparr): Add a setter method to control streaming settings (e.g., + // resolution and FPS). + Stream onStreamedFrameAvailable(int cameraId) { + throw UnimplementedError('onStreamedFrameAvailable() is not implemented.'); + } + /// Sets the flash mode for the selected camera. /// On Web [FlashMode.auto] corresponds to [FlashMode.always]. Future setFlashMode(int cameraId, FlashMode mode) { diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart index 7da4262cdf79..d096f0012c86 100644 --- a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -1038,6 +1038,52 @@ void main() { arguments: {'cameraId': cameraId}), ]); }); + + test('Should start streaming', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'startImageStream': null, + 'stopImageStream': null, + }, + ); + + // Act + final StreamSubscription subscription = camera + .onStreamedFrameAvailable(cameraId) + .listen((CameraImageData imageData) {}); + + // Assert + expect(channel.log, [ + isMethodCall('startImageStream', arguments: null), + ]); + + subscription.cancel(); + }); + + test('Should stop streaming', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'startImageStream': null, + 'stopImageStream': null, + }, + ); + + // Act + final StreamSubscription subscription = camera + .onStreamedFrameAvailable(cameraId) + .listen((CameraImageData imageData) {}); + subscription.cancel(); + + // Assert + expect(channel.log, [ + isMethodCall('startImageStream', arguments: null), + isMethodCall('stopImageStream', arguments: null), + ]); + }); }); }); } diff --git a/packages/camera/camera_platform_interface/test/method_channel/type_conversion_test.dart b/packages/camera/camera_platform_interface/test/method_channel/type_conversion_test.dart new file mode 100644 index 000000000000..a8ca45eca43b --- /dev/null +++ b/packages/camera/camera_platform_interface/test/method_channel/type_conversion_test.dart @@ -0,0 +1,85 @@ +// 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:typed_data'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/method_channel/type_conversion.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('CameraImageData can be created', () { + final CameraImageData cameraImage = + cameraImageFromPlatformData({ + 'format': 35, + 'height': 1, + 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.height, 1); + expect(cameraImage.width, 4); + expect(cameraImage.format.group, ImageFormatGroup.yuv420); + expect(cameraImage.planes.length, 1); + }); + + test('CameraImageData has ImageFormatGroup.yuv420 for iOS', () { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + + final CameraImageData cameraImage = + cameraImageFromPlatformData({ + 'format': 875704438, + 'height': 1, + 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.format.group, ImageFormatGroup.yuv420); + }); + + test('CameraImageData has ImageFormatGroup.yuv420 for Android', () { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final CameraImageData cameraImage = + cameraImageFromPlatformData({ + 'format': 35, + 'height': 1, + 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.format.group, ImageFormatGroup.yuv420); + }); +} From ea8c032a47d912ec3d90dd4d346424b96f20e11c Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 18 May 2022 10:31:02 -0400 Subject: [PATCH 03/10] Rewire app-facing package --- .../camera/lib/src/camera_controller.dart | 32 +++++------------ .../camera/camera/lib/src/camera_image.dart | 34 ++++++++++++++++++- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 5014795320f2..6566e2abc883 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -12,8 +12,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:quiver/core.dart'; -const MethodChannel _channel = MethodChannel('plugins.flutter.io/camera'); - /// Signature for a callback receiving the a camera image. /// /// This is used by [CameraController.startImageStream]. @@ -257,7 +255,7 @@ class CameraController extends ValueNotifier { int _cameraId = kUninitializedCameraId; bool _isDisposed = false; - StreamSubscription? _imageStreamSubscription; + StreamSubscription? _imageStreamSubscription; FutureOr? _initCalled; StreamSubscription? _deviceOrientationSubscription; @@ -438,27 +436,15 @@ class CameraController extends ValueNotifier { } try { - await _channel.invokeMethod('startImageStream'); + _imageStreamSubscription = CameraPlatform.instance + .onStreamedFrameAvailable(_cameraId) + .listen((CameraImageData imageData) { + onAvailable(CameraImage.fromPlatformInterface(imageData)); + }); value = value.copyWith(isStreamingImages: true); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } - const EventChannel cameraEventChannel = - EventChannel('plugins.flutter.io/camera/imageStream'); - _imageStreamSubscription = - cameraEventChannel.receiveBroadcastStream().listen( - (dynamic imageData) { - if (defaultTargetPlatform == TargetPlatform.iOS) { - try { - _channel.invokeMethod('receivedImageStreamData'); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } - } - onAvailable( - CameraImage.fromPlatformData(imageData as Map)); - }, - ); } /// Stop streaming images from platform camera. @@ -487,13 +473,11 @@ class CameraController extends ValueNotifier { try { value = value.copyWith(isStreamingImages: false); - await _channel.invokeMethod('stopImageStream'); + await _imageStreamSubscription?.cancel(); + _imageStreamSubscription = null; } on PlatformException catch (e) { throw CameraException(e.code, e.message); } - - await _imageStreamSubscription?.cancel(); - _imageStreamSubscription = null; } /// Start a video recording. diff --git a/packages/camera/camera/lib/src/camera_image.dart b/packages/camera/camera/lib/src/camera_image.dart index 0f2377ed170c..3032e7af2025 100644 --- a/packages/camera/camera/lib/src/camera_image.dart +++ b/packages/camera/camera/lib/src/camera_image.dart @@ -7,11 +7,23 @@ import 'dart:typed_data'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/foundation.dart'; +// TODO(stuartmorgan): Remove all of these classes in a breaking change, and +// vend the platform interface versions directly. + /// A single color plane of image data. /// /// The number and meaning of the planes in an image are determined by the /// format of the Image. class Plane { + Plane._fromPlatformInterface(CameraImagePlane plane) + : bytes = plane.bytes, + bytesPerPixel = plane.bytesPerPixel, + bytesPerRow = plane.bytesPerRow, + height = plane.height, + width = plane.width; + + // Only used by the deprecated codepath that's kept to avoid breaking changes. + // Never called by the plugin itself. Plane._fromPlatformData(Map data) : bytes = data['bytes'] as Uint8List, bytesPerPixel = data['bytesPerPixel'] as int?, @@ -43,6 +55,12 @@ class Plane { /// Describes how pixels are represented in an image. class ImageFormat { + ImageFormat._fromPlatformInterface(CameraImageFormat format) + : group = format.group, + raw = format.raw; + + // Only used by the deprecated codepath that's kept to avoid breaking changes. + // Never called by the plugin itself. ImageFormat._fromPlatformData(this.raw) : group = _asImageFormatGroup(raw); /// Describes the format group the raw image format falls into. @@ -58,6 +76,8 @@ class ImageFormat { final dynamic raw; } +// Only used by the deprecated codepath that's kept to avoid breaking changes. +// Never called by the plugin itself. ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) { if (defaultTargetPlatform == TargetPlatform.android) { switch (rawFormat) { @@ -94,7 +114,19 @@ ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) { /// Although not all image formats are planar on iOS, we treat 1-dimensional /// images as single planar images. class CameraImage { - /// CameraImage Constructor + /// Creates a [CameraImage] from the platform interface version. + CameraImage.fromPlatformInterface(CameraImageData data) + : format = ImageFormat._fromPlatformInterface(data.format), + height = data.height, + width = data.width, + planes = List.unmodifiable(data.planes.map( + (CameraImagePlane plane) => Plane._fromPlatformInterface(plane))), + lensAperture = data.lensAperture, + sensorExposureTime = data.sensorExposureTime, + sensorSensitivity = data.sensorSensitivity; + + /// Creates a [CameraImage] from method channel data. + @Deprecated('Use fromPlatformInterface instead') CameraImage.fromPlatformData(Map data) : format = ImageFormat._fromPlatformData(data['format']), height = data['height'] as int, From 7a84131be238eae7e3e81d79b7c595e19ce827f8 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 18 May 2022 12:22:07 -0400 Subject: [PATCH 04/10] Test update --- .../camera/test/camera_image_stream_test.dart | 69 +++++++++---------- .../camera/camera/test/camera_image_test.dart | 55 ++++++++++++++- .../test/utils/method_channel_mock.dart | 39 ----------- 3 files changed, 88 insertions(+), 75 deletions(-) delete mode 100644 packages/camera/camera/test/utils/method_channel_mock.dart diff --git a/packages/camera/camera/test/camera_image_stream_test.dart b/packages/camera/camera/test/camera_image_stream_test.dart index 7055b2239a5a..f7ee9dc48371 100644 --- a/packages/camera/camera/test/camera_image_stream_test.dart +++ b/packages/camera/camera/test/camera_image_stream_test.dart @@ -2,18 +2,21 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:camera/camera.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; import 'camera_test.dart'; -import 'utils/method_channel_mock.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); + late MockStreamingCameraPlatform mockPlatform; setUp(() { - CameraPlatform.instance = MockCameraPlatform(); + mockPlatform = MockStreamingCameraPlatform(); + CameraPlatform.instance = mockPlatform; }); test('startImageStream() throws $CameraException when uninitialized', () { @@ -87,13 +90,6 @@ void main() { }); test('startImageStream() calls CameraPlatform', () async { - final MethodChannelMock cameraChannelMock = MethodChannelMock( - channelName: 'plugins.flutter.io/camera', - methods: {'startImageStream': {}}); - final MethodChannelMock streamChannelMock = MethodChannelMock( - channelName: 'plugins.flutter.io/camera/imageStream', - methods: {'listen': {}}); - final CameraController cameraController = CameraController( const CameraDescription( name: 'cam', @@ -104,10 +100,8 @@ void main() { await cameraController.startImageStream((CameraImage image) => null); - expect(cameraChannelMock.log, - [isMethodCall('startImageStream', arguments: null)]); - expect(streamChannelMock.log, - [isMethodCall('listen', arguments: null)]); + expect(mockPlatform.streamCallLog, + ['onStreamedFrameAvailable', 'listen']); }); test('stopImageStream() throws $CameraException when uninitialized', () { @@ -178,19 +172,6 @@ void main() { }); test('stopImageStream() intended behaviour', () async { - final MethodChannelMock cameraChannelMock = MethodChannelMock( - channelName: 'plugins.flutter.io/camera', - methods: { - 'startImageStream': {}, - 'stopImageStream': {} - }); - final MethodChannelMock streamChannelMock = MethodChannelMock( - channelName: 'plugins.flutter.io/camera/imageStream', - methods: { - 'listen': {}, - 'cancel': {} - }); - final CameraController cameraController = CameraController( const CameraDescription( name: 'cam', @@ -201,14 +182,32 @@ void main() { await cameraController.startImageStream((CameraImage image) => null); await cameraController.stopImageStream(); - expect(cameraChannelMock.log, [ - isMethodCall('startImageStream', arguments: null), - isMethodCall('stopImageStream', arguments: null) - ]); - - expect(streamChannelMock.log, [ - isMethodCall('listen', arguments: null), - isMethodCall('cancel', arguments: null) - ]); + expect(mockPlatform.streamCallLog, + ['onStreamedFrameAvailable', 'listen', 'cancel']); }); } + +class MockStreamingCameraPlatform extends MockCameraPlatform { + List streamCallLog = []; + + StreamController? _streamController; + + @override + Stream onStreamedFrameAvailable(int cameraId) { + streamCallLog.add('onStreamedFrameAvailable'); + _streamController = StreamController( + onListen: _onFrameStreamListen, + onCancel: _onFrameStreamCancel, + ); + return _streamController!.stream; + } + + void _onFrameStreamListen() { + streamCallLog.add('listen'); + } + + FutureOr _onFrameStreamCancel() async { + streamCallLog.add('cancel'); + _streamController = null; + } +} diff --git a/packages/camera/camera/test/camera_image_test.dart b/packages/camera/camera/test/camera_image_test.dart index 55bf4a2727e2..27bbe1739129 100644 --- a/packages/camera/camera/test/camera_image_test.dart +++ b/packages/camera/camera/test/camera_image_test.dart @@ -5,11 +5,64 @@ import 'dart:typed_data'; import 'package:camera/camera.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - group('$CameraImage tests', () { + test('translates correctly from platform interface classes', () { + final CameraImageData originalImage = CameraImageData( + format: const CameraImageFormat(ImageFormatGroup.jpeg, raw: 1234), + planes: [ + CameraImagePlane( + bytes: Uint8List.fromList([1, 2, 3, 4]), + bytesPerRow: 20, + bytesPerPixel: 3, + width: 200, + height: 100, + ), + CameraImagePlane( + bytes: Uint8List.fromList([5, 6, 7, 8]), + bytesPerRow: 18, + bytesPerPixel: 4, + width: 220, + height: 110, + ), + ], + width: 640, + height: 480, + lensAperture: 2.5, + sensorExposureTime: 5, + sensorSensitivity: 1.3, + ); + + final CameraImage image = CameraImage.fromPlatformInterface(originalImage); + // Simple values. + expect(image.width, originalImage.width); + expect(image.height, originalImage.height); + expect(image.lensAperture, originalImage.lensAperture); + expect(image.sensorExposureTime, originalImage.sensorExposureTime); + expect(image.sensorSensitivity, originalImage.sensorSensitivity); + // Format. + expect(image.format.group, originalImage.format.group); + expect(image.format.raw, originalImage.format.raw); + // Planes. + expect(image.planes.length, originalImage.planes.length); + for (int i = 0; i < image.planes.length; i++) { + expect( + image.planes[i].bytes.length, originalImage.planes[i].bytes.length); + for (int j = 0; j < image.planes[i].bytes.length; j++) { + expect(image.planes[i].bytes[j], originalImage.planes[i].bytes[j]); + } + expect( + image.planes[i].bytesPerPixel, originalImage.planes[i].bytesPerPixel); + expect(image.planes[i].bytesPerRow, originalImage.planes[i].bytesPerRow); + expect(image.planes[i].width, originalImage.planes[i].width); + expect(image.planes[i].height, originalImage.planes[i].height); + } + }); + + group('legacy constructors', () { test('$CameraImage can be created', () { debugDefaultTargetPlatformOverride = TargetPlatform.android; final CameraImage cameraImage = diff --git a/packages/camera/camera/test/utils/method_channel_mock.dart b/packages/camera/camera/test/utils/method_channel_mock.dart deleted file mode 100644 index 7c8b4ca3d3f0..000000000000 --- a/packages/camera/camera/test/utils/method_channel_mock.dart +++ /dev/null @@ -1,39 +0,0 @@ -// 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:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -class MethodChannelMock { - MethodChannelMock({ - required String channelName, - this.delay, - required this.methods, - }) : methodChannel = MethodChannel(channelName) { - methodChannel.setMockMethodCallHandler(_handler); - } - - final Duration? delay; - final MethodChannel methodChannel; - final Map methods; - final List log = []; - - Future _handler(MethodCall methodCall) async { - log.add(methodCall); - - if (!methods.containsKey(methodCall.method)) { - throw MissingPluginException('No implementation found for method ' - '${methodCall.method} on channel ${methodChannel.name}'); - } - - return Future.delayed(delay ?? Duration.zero, () { - final Object? result = methods[methodCall.method]; - if (result is Exception) { - throw result; - } - - return Future.value(result); - }); - } -} From 3b6865c0d3727dce4658d76e7b07a9a1335b65ff Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 18 May 2022 13:19:52 -0400 Subject: [PATCH 05/10] Temporary pubspec overrides --- packages/camera/camera/example/pubspec.yaml | 5 +++++ packages/camera/camera/pubspec.yaml | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml index af4d078ff836..0b1148960638 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -30,3 +30,8 @@ dev_dependencies: flutter: uses-material-design: true + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + camera_platform_interface: + path: ../../../camera/camera_platform_interface diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 14acf32e2324..44bb5ac84a65 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -37,3 +37,9 @@ dev_dependencies: mockito: ^5.0.0 plugin_platform_interface: ^2.0.0 video_player: ^2.0.0 + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + camera_platform_interface: + path: ../../camera/camera_platform_interface From 7414e48cacc1d8f63b684f4673eb4cffd3a0c8ee Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 18 May 2022 13:22:25 -0400 Subject: [PATCH 06/10] Version bumps --- packages/camera/camera/CHANGELOG.md | 4 ++++ packages/camera/camera/pubspec.yaml | 4 ++-- packages/camera/camera_platform_interface/CHANGELOG.md | 3 ++- packages/camera/camera_platform_interface/pubspec.yaml | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index d101f60cf041..4d2e933e9ec6 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.5+2 + +* Moves streaming implementation to the platform interface package. + ## 0.9.5+1 * Suppresses warnings for pre-iOS-11 codepaths. diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 44bb5ac84a65..5c9d93cdb41c 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing Dart. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.5+1 +version: 0.9.5+2 environment: sdk: ">=2.14.0 <3.0.0" @@ -22,7 +22,7 @@ flutter: default_package: camera_web dependencies: - camera_platform_interface: ^2.1.0 + camera_platform_interface: ^2.2.0 camera_web: ^0.2.1 flutter: sdk: flutter diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index 3cad35d71ae5..5ecd8891fe20 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.2.0 +* Adds image streaming to the platform interface. * Removes unnecessary imports. ## 2.1.6 diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index ab163b4e9f3f..473dcb552c82 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.1.6 +version: 2.2.0 environment: sdk: '>=2.12.0 <3.0.0' From b191e3455c02ff738fc5543d394e45c3e3fa624b Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 18 May 2022 13:36:48 -0400 Subject: [PATCH 07/10] Future-proof the new API with an options dictionary --- packages/camera/camera/test/camera_image_stream_test.dart | 3 ++- .../lib/src/method_channel/method_channel_camera.dart | 3 ++- .../lib/src/platform_interface/camera_platform.dart | 5 +++-- .../lib/src/types/camera_image_data.dart | 7 +++++++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/camera/camera/test/camera_image_stream_test.dart b/packages/camera/camera/test/camera_image_stream_test.dart index f7ee9dc48371..a9320e46dfb5 100644 --- a/packages/camera/camera/test/camera_image_stream_test.dart +++ b/packages/camera/camera/test/camera_image_stream_test.dart @@ -193,7 +193,8 @@ class MockStreamingCameraPlatform extends MockCameraPlatform { StreamController? _streamController; @override - Stream onStreamedFrameAvailable(int cameraId) { + Stream onStreamedFrameAvailable(int cameraId, + {CameraImageStreamOptions? options}) { streamCallLog.add('onStreamedFrameAvailable'); _streamController = StreamController( onListen: _onFrameStreamListen, diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart index e2b9e4bfa505..babef144b086 100644 --- a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -276,7 +276,8 @@ class MethodChannelCamera extends CameraPlatform { ); @override - Stream onStreamedFrameAvailable(int cameraId) { + Stream onStreamedFrameAvailable(int cameraId, + {CameraImageStreamOptions? options}) { _frameStreamController = StreamController( onListen: _onFrameStreamListen, onPause: _onFrameStreamPauseResume, 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 e5946ec4a117..eaa779a943db 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 @@ -157,9 +157,10 @@ abstract class CameraPlatform extends PlatformInterface { /// listen again later. /// /// - // TODO(bmparr): Add a setter method to control streaming settings (e.g., + // TODO(bmparr): Add options to control streaming settings (e.g., // resolution and FPS). - Stream onStreamedFrameAvailable(int cameraId) { + Stream onStreamedFrameAvailable(int cameraId, + {CameraImageStreamOptions? options}) { throw UnimplementedError('onStreamedFrameAvailable() is not implemented.'); } diff --git a/packages/camera/camera_platform_interface/lib/src/types/camera_image_data.dart b/packages/camera/camera_platform_interface/lib/src/types/camera_image_data.dart index c7deab6cc495..8d010fb80eef 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/camera_image_data.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/camera_image_data.dart @@ -8,6 +8,13 @@ import 'package:flutter/foundation.dart'; import '../../camera_platform_interface.dart'; +/// Options for configuring camera streaming. +/// +/// Currently unused; this exists for future-proofing of the platform interface +/// API. +@immutable +class CameraImageStreamOptions {} + /// A single color plane of image data. /// /// The number and meaning of the planes in an image are determined by its From 014f1c05b5abcea678a548175c7f02989a8c9e96 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 19 May 2022 10:28:37 -0400 Subject: [PATCH 08/10] Review comments --- packages/camera/camera/lib/src/camera_image.dart | 3 ++- packages/camera/camera/test/camera_image_test.dart | 14 +++++++------- .../lib/src/types/camera_image_data.dart | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/camera/camera/lib/src/camera_image.dart b/packages/camera/camera/lib/src/camera_image.dart index 3032e7af2025..cb3d306eaf6e 100644 --- a/packages/camera/camera/lib/src/camera_image.dart +++ b/packages/camera/camera/lib/src/camera_image.dart @@ -8,7 +8,8 @@ import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/foundation.dart'; // TODO(stuartmorgan): Remove all of these classes in a breaking change, and -// vend the platform interface versions directly. +// vend the platform interface versions directly. See +// https://github.com/flutter/flutter/issues/104188 /// A single color plane of image data. /// diff --git a/packages/camera/camera/test/camera_image_test.dart b/packages/camera/camera/test/camera_image_test.dart index 27bbe1739129..c964e7acd97b 100644 --- a/packages/camera/camera/test/camera_image_test.dart +++ b/packages/camera/camera/test/camera_image_test.dart @@ -38,14 +38,14 @@ void main() { final CameraImage image = CameraImage.fromPlatformInterface(originalImage); // Simple values. - expect(image.width, originalImage.width); - expect(image.height, originalImage.height); - expect(image.lensAperture, originalImage.lensAperture); - expect(image.sensorExposureTime, originalImage.sensorExposureTime); - expect(image.sensorSensitivity, originalImage.sensorSensitivity); + expect(image.width, 640); + expect(image.height, 480); + expect(image.lensAperture, 2.5); + expect(image.sensorExposureTime, 5); + expect(image.sensorSensitivity, 1.3); // Format. - expect(image.format.group, originalImage.format.group); - expect(image.format.raw, originalImage.format.raw); + expect(image.format.group, ImageFormatGroup.jpeg); + expect(image.format.raw, 1234); // Planes. expect(image.planes.length, originalImage.planes.length); for (int i = 0; i < image.planes.length; i++) { diff --git a/packages/camera/camera_platform_interface/lib/src/types/camera_image_data.dart b/packages/camera/camera_platform_interface/lib/src/types/camera_image_data.dart index 8d010fb80eef..6971dbb39737 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/camera_image_data.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/camera_image_data.dart @@ -39,10 +39,10 @@ class CameraImagePlane { /// The distance between adjacent pixel samples in bytes, when available. final int? bytesPerPixel; - /// Height of the pixel buffer, when available + /// Height of the pixel buffer, when available. final int? height; - /// Width of the pixel buffer, when available + /// Width of the pixel buffer, when available. final int? width; } From 227cc317ceaa3abfff6b239ffa90a136391992f2 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 25 May 2022 15:48:57 -0400 Subject: [PATCH 09/10] Remove override --- packages/camera/camera/pubspec.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 35408c2c6077..d1f70d906626 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -37,9 +37,3 @@ dev_dependencies: mockito: ^5.0.0 plugin_platform_interface: ^2.0.0 video_player: ^2.0.0 - - -# FOR TESTING ONLY. DO NOT MERGE. -dependency_overrides: - camera_platform_interface: - path: ../../camera/camera_platform_interface From 11025be3a8b22f5c8baad03996b22c9eca404fe7 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 25 May 2022 15:51:23 -0400 Subject: [PATCH 10/10] Remove example override --- packages/camera/camera/example/pubspec.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml index 429be9407f0a..e9ae2c74a6be 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -30,8 +30,3 @@ dev_dependencies: flutter: uses-material-design: true - -# FOR TESTING ONLY. DO NOT MERGE. -dependency_overrides: - camera_platform_interface: - path: ../../../camera/camera_platform_interface