[camera] Switch to platform-interface-provided streaming (#5833)
diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index bd42ef4..72af38a 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 5014795..6566e2a 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/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 @@
int _cameraId = kUninitializedCameraId;
bool _isDisposed = false;
- StreamSubscription<dynamic>? _imageStreamSubscription;
+ StreamSubscription<CameraImageData>? _imageStreamSubscription;
FutureOr<bool>? _initCalled;
StreamSubscription<DeviceOrientationChangedEvent>?
_deviceOrientationSubscription;
@@ -438,27 +436,15 @@
}
try {
- await _channel.invokeMethod<void>('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<void>('receivedImageStreamData');
- } on PlatformException catch (e) {
- throw CameraException(e.code, e.message);
- }
- }
- onAvailable(
- CameraImage.fromPlatformData(imageData as Map<dynamic, dynamic>));
- },
- );
}
/// Stop streaming images from platform camera.
@@ -487,13 +473,11 @@
try {
value = value.copyWith(isStreamingImages: false);
- await _channel.invokeMethod<void>('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 0f2377e..cb3d306 100644
--- a/packages/camera/camera/lib/src/camera_image.dart
+++ b/packages/camera/camera/lib/src/camera_image.dart
@@ -7,11 +7,24 @@
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<dynamic, dynamic> data)
: bytes = data['bytes'] as Uint8List,
bytesPerPixel = data['bytesPerPixel'] as int?,
@@ -43,6 +56,12 @@
/// 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 @@
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 @@
/// 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<Plane>.unmodifiable(data.planes.map<Plane>(
+ (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<dynamic, dynamic> 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 ea9f2e0..d1f70d9 100644
--- a/packages/camera/camera/pubspec.yaml
+++ b/packages/camera/camera/pubspec.yaml
@@ -4,7 +4,7 @@
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 @@
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 7055b22..a9320e4 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 @@
});
test('startImageStream() calls CameraPlatform', () async {
- final MethodChannelMock cameraChannelMock = MethodChannelMock(
- channelName: 'plugins.flutter.io/camera',
- methods: <String, dynamic>{'startImageStream': <String, dynamic>{}});
- final MethodChannelMock streamChannelMock = MethodChannelMock(
- channelName: 'plugins.flutter.io/camera/imageStream',
- methods: <String, dynamic>{'listen': <String, dynamic>{}});
-
final CameraController cameraController = CameraController(
const CameraDescription(
name: 'cam',
@@ -104,10 +100,8 @@
await cameraController.startImageStream((CameraImage image) => null);
- expect(cameraChannelMock.log,
- <Matcher>[isMethodCall('startImageStream', arguments: null)]);
- expect(streamChannelMock.log,
- <Matcher>[isMethodCall('listen', arguments: null)]);
+ expect(mockPlatform.streamCallLog,
+ <String>['onStreamedFrameAvailable', 'listen']);
});
test('stopImageStream() throws $CameraException when uninitialized', () {
@@ -178,19 +172,6 @@
});
test('stopImageStream() intended behaviour', () async {
- final MethodChannelMock cameraChannelMock = MethodChannelMock(
- channelName: 'plugins.flutter.io/camera',
- methods: <String, dynamic>{
- 'startImageStream': <String, dynamic>{},
- 'stopImageStream': <String, dynamic>{}
- });
- final MethodChannelMock streamChannelMock = MethodChannelMock(
- channelName: 'plugins.flutter.io/camera/imageStream',
- methods: <String, dynamic>{
- 'listen': <String, dynamic>{},
- 'cancel': <String, dynamic>{}
- });
-
final CameraController cameraController = CameraController(
const CameraDescription(
name: 'cam',
@@ -201,14 +182,33 @@
await cameraController.startImageStream((CameraImage image) => null);
await cameraController.stopImageStream();
- expect(cameraChannelMock.log, <Matcher>[
- isMethodCall('startImageStream', arguments: null),
- isMethodCall('stopImageStream', arguments: null)
- ]);
-
- expect(streamChannelMock.log, <Matcher>[
- isMethodCall('listen', arguments: null),
- isMethodCall('cancel', arguments: null)
- ]);
+ expect(mockPlatform.streamCallLog,
+ <String>['onStreamedFrameAvailable', 'listen', 'cancel']);
});
}
+
+class MockStreamingCameraPlatform extends MockCameraPlatform {
+ List<String> streamCallLog = <String>[];
+
+ StreamController<CameraImageData>? _streamController;
+
+ @override
+ Stream<CameraImageData> onStreamedFrameAvailable(int cameraId,
+ {CameraImageStreamOptions? options}) {
+ streamCallLog.add('onStreamedFrameAvailable');
+ _streamController = StreamController<CameraImageData>(
+ onListen: _onFrameStreamListen,
+ onCancel: _onFrameStreamCancel,
+ );
+ return _streamController!.stream;
+ }
+
+ void _onFrameStreamListen() {
+ streamCallLog.add('listen');
+ }
+
+ FutureOr<void> _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 55bf4a2..c964e7a 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>[
+ CameraImagePlane(
+ bytes: Uint8List.fromList(<int>[1, 2, 3, 4]),
+ bytesPerRow: 20,
+ bytesPerPixel: 3,
+ width: 200,
+ height: 100,
+ ),
+ CameraImagePlane(
+ bytes: Uint8List.fromList(<int>[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 7c8b4ca..0000000
--- 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<String, dynamic> methods;
- final List<MethodCall> log = <MethodCall>[];
-
- Future<dynamic> _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<dynamic>.delayed(delay ?? Duration.zero, () {
- final Object? result = methods[methodCall.method];
- if (result is Exception) {
- throw result;
- }
-
- return Future<dynamic>.value(result);
- });
- }
-}