Skip to content

Commit

Permalink
[camera] Switch to platform-interface-provided streaming (flutter#5833)
Browse files Browse the repository at this point in the history
  • Loading branch information
stuartmorgan authored and mauricioluz committed Jan 26, 2023
1 parent 9146d72 commit 6d617fe
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 102 deletions.
4 changes: 4 additions & 0 deletions packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
32 changes: 8 additions & 24 deletions packages/camera/camera/lib/src/camera_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down Expand Up @@ -257,7 +255,7 @@ class CameraController extends ValueNotifier<CameraValue> {
int _cameraId = kUninitializedCameraId;

bool _isDisposed = false;
StreamSubscription<dynamic>? _imageStreamSubscription;
StreamSubscription<CameraImageData>? _imageStreamSubscription;
FutureOr<bool>? _initCalled;
StreamSubscription<DeviceOrientationChangedEvent>?
_deviceOrientationSubscription;
Expand Down Expand Up @@ -438,27 +436,15 @@ class CameraController extends ValueNotifier<CameraValue> {
}

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.
Expand Down Expand Up @@ -487,13 +473,11 @@ class CameraController extends ValueNotifier<CameraValue> {

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.
Expand Down
35 changes: 34 additions & 1 deletion packages/camera/camera/lib/src/camera_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<dynamic, dynamic> data)
: bytes = data['bytes'] as Uint8List,
bytesPerPixel = data['bytesPerPixel'] as int?,
Expand Down Expand Up @@ -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.
Expand All @@ -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) {
Expand Down Expand Up @@ -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<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,
Expand Down
4 changes: 2 additions & 2 deletions packages/camera/camera/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
70 changes: 35 additions & 35 deletions packages/camera/camera/test/camera_image_stream_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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', () {
Expand Down Expand Up @@ -87,13 +90,6 @@ void main() {
});

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',
Expand All @@ -104,10 +100,8 @@ void main() {

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', () {
Expand Down Expand Up @@ -178,19 +172,6 @@ void main() {
});

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',
Expand All @@ -201,14 +182,33 @@ void main() {
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;
}
}
55 changes: 54 additions & 1 deletion packages/camera/camera/test/camera_image_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
39 changes: 0 additions & 39 deletions packages/camera/camera/test/utils/method_channel_mock.dart

This file was deleted.

0 comments on commit 6d617fe

Please sign in to comment.