diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index bd42ef441287..72af38a9f9de 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.7+1 + +* Moves streaming implementation to the platform interface package. + ## 0.9.7 * Returns all the available cameras on iOS. 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..cb3d306eaf6e 100644 --- a/packages/camera/camera/lib/src/camera_image.dart +++ b/packages/camera/camera/lib/src/camera_image.dart @@ -7,11 +7,24 @@ 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. See +// https://github.com/flutter/flutter/issues/104188 + /// 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 +56,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 +77,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 +115,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, diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index ea9f2e036161..d1f70d906626 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.7 +version: 0.9.7+1 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/test/camera_image_stream_test.dart b/packages/camera/camera/test/camera_image_stream_test.dart index 7055b2239a5a..a9320e46dfb5 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,33 @@ 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, + {CameraImageStreamOptions? options}) { + 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..c964e7acd97b 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, 640); + expect(image.height, 480); + expect(image.lensAperture, 2.5); + expect(image.sensorExposureTime, 5); + expect(image.sensorSensitivity, 1.3); + // Format. + 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++) { + 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); - }); - } -}