Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[camera] Switch to platform-interface-provided streaming #5833

Merged
merged 11 commits into from
May 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.