Skip to content

Commit 6d617fe

Browse files
stuartmorganmauricioluz
authored andcommitted
[camera] Switch to platform-interface-provided streaming (flutter#5833)
1 parent 9146d72 commit 6d617fe

7 files changed

+137
-102
lines changed

packages/camera/camera/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.7+1
2+
3+
* Moves streaming implementation to the platform interface package.
4+
15
## 0.9.7
26

37
* Returns all the available cameras on iOS.

packages/camera/camera/lib/src/camera_controller.dart

+8-24
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ import 'package:flutter/material.dart';
1212
import 'package:flutter/services.dart';
1313
import 'package:quiver/core.dart';
1414

15-
const MethodChannel _channel = MethodChannel('plugins.flutter.io/camera');
16-
1715
/// Signature for a callback receiving the a camera image.
1816
///
1917
/// This is used by [CameraController.startImageStream].
@@ -257,7 +255,7 @@ class CameraController extends ValueNotifier<CameraValue> {
257255
int _cameraId = kUninitializedCameraId;
258256

259257
bool _isDisposed = false;
260-
StreamSubscription<dynamic>? _imageStreamSubscription;
258+
StreamSubscription<CameraImageData>? _imageStreamSubscription;
261259
FutureOr<bool>? _initCalled;
262260
StreamSubscription<DeviceOrientationChangedEvent>?
263261
_deviceOrientationSubscription;
@@ -438,27 +436,15 @@ class CameraController extends ValueNotifier<CameraValue> {
438436
}
439437

440438
try {
441-
await _channel.invokeMethod<void>('startImageStream');
439+
_imageStreamSubscription = CameraPlatform.instance
440+
.onStreamedFrameAvailable(_cameraId)
441+
.listen((CameraImageData imageData) {
442+
onAvailable(CameraImage.fromPlatformInterface(imageData));
443+
});
442444
value = value.copyWith(isStreamingImages: true);
443445
} on PlatformException catch (e) {
444446
throw CameraException(e.code, e.message);
445447
}
446-
const EventChannel cameraEventChannel =
447-
EventChannel('plugins.flutter.io/camera/imageStream');
448-
_imageStreamSubscription =
449-
cameraEventChannel.receiveBroadcastStream().listen(
450-
(dynamic imageData) {
451-
if (defaultTargetPlatform == TargetPlatform.iOS) {
452-
try {
453-
_channel.invokeMethod<void>('receivedImageStreamData');
454-
} on PlatformException catch (e) {
455-
throw CameraException(e.code, e.message);
456-
}
457-
}
458-
onAvailable(
459-
CameraImage.fromPlatformData(imageData as Map<dynamic, dynamic>));
460-
},
461-
);
462448
}
463449

464450
/// Stop streaming images from platform camera.
@@ -487,13 +473,11 @@ class CameraController extends ValueNotifier<CameraValue> {
487473

488474
try {
489475
value = value.copyWith(isStreamingImages: false);
490-
await _channel.invokeMethod<void>('stopImageStream');
476+
await _imageStreamSubscription?.cancel();
477+
_imageStreamSubscription = null;
491478
} on PlatformException catch (e) {
492479
throw CameraException(e.code, e.message);
493480
}
494-
495-
await _imageStreamSubscription?.cancel();
496-
_imageStreamSubscription = null;
497481
}
498482

499483
/// Start a video recording.

packages/camera/camera/lib/src/camera_image.dart

+34-1
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,24 @@ import 'dart:typed_data';
77
import 'package:camera_platform_interface/camera_platform_interface.dart';
88
import 'package:flutter/foundation.dart';
99

10+
// TODO(stuartmorgan): Remove all of these classes in a breaking change, and
11+
// vend the platform interface versions directly. See
12+
// https://github.com/flutter/flutter/issues/104188
13+
1014
/// A single color plane of image data.
1115
///
1216
/// The number and meaning of the planes in an image are determined by the
1317
/// format of the Image.
1418
class Plane {
19+
Plane._fromPlatformInterface(CameraImagePlane plane)
20+
: bytes = plane.bytes,
21+
bytesPerPixel = plane.bytesPerPixel,
22+
bytesPerRow = plane.bytesPerRow,
23+
height = plane.height,
24+
width = plane.width;
25+
26+
// Only used by the deprecated codepath that's kept to avoid breaking changes.
27+
// Never called by the plugin itself.
1528
Plane._fromPlatformData(Map<dynamic, dynamic> data)
1629
: bytes = data['bytes'] as Uint8List,
1730
bytesPerPixel = data['bytesPerPixel'] as int?,
@@ -43,6 +56,12 @@ class Plane {
4356

4457
/// Describes how pixels are represented in an image.
4558
class ImageFormat {
59+
ImageFormat._fromPlatformInterface(CameraImageFormat format)
60+
: group = format.group,
61+
raw = format.raw;
62+
63+
// Only used by the deprecated codepath that's kept to avoid breaking changes.
64+
// Never called by the plugin itself.
4665
ImageFormat._fromPlatformData(this.raw) : group = _asImageFormatGroup(raw);
4766

4867
/// Describes the format group the raw image format falls into.
@@ -58,6 +77,8 @@ class ImageFormat {
5877
final dynamic raw;
5978
}
6079

80+
// Only used by the deprecated codepath that's kept to avoid breaking changes.
81+
// Never called by the plugin itself.
6182
ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) {
6283
if (defaultTargetPlatform == TargetPlatform.android) {
6384
switch (rawFormat) {
@@ -94,7 +115,19 @@ ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) {
94115
/// Although not all image formats are planar on iOS, we treat 1-dimensional
95116
/// images as single planar images.
96117
class CameraImage {
97-
/// CameraImage Constructor
118+
/// Creates a [CameraImage] from the platform interface version.
119+
CameraImage.fromPlatformInterface(CameraImageData data)
120+
: format = ImageFormat._fromPlatformInterface(data.format),
121+
height = data.height,
122+
width = data.width,
123+
planes = List<Plane>.unmodifiable(data.planes.map<Plane>(
124+
(CameraImagePlane plane) => Plane._fromPlatformInterface(plane))),
125+
lensAperture = data.lensAperture,
126+
sensorExposureTime = data.sensorExposureTime,
127+
sensorSensitivity = data.sensorSensitivity;
128+
129+
/// Creates a [CameraImage] from method channel data.
130+
@Deprecated('Use fromPlatformInterface instead')
98131
CameraImage.fromPlatformData(Map<dynamic, dynamic> data)
99132
: format = ImageFormat._fromPlatformData(data['format']),
100133
height = data['height'] as int,

packages/camera/camera/pubspec.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing
44
Dart.
55
repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera
66
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
7-
version: 0.9.7
7+
version: 0.9.7+1
88

99
environment:
1010
sdk: ">=2.14.0 <3.0.0"
@@ -22,7 +22,7 @@ flutter:
2222
default_package: camera_web
2323

2424
dependencies:
25-
camera_platform_interface: ^2.1.0
25+
camera_platform_interface: ^2.2.0
2626
camera_web: ^0.2.1
2727
flutter:
2828
sdk: flutter

packages/camera/camera/test/camera_image_stream_test.dart

+35-35
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:async';
6+
57
import 'package:camera/camera.dart';
68
import 'package:camera_platform_interface/camera_platform_interface.dart';
79
import 'package:flutter_test/flutter_test.dart';
810

911
import 'camera_test.dart';
10-
import 'utils/method_channel_mock.dart';
1112

1213
void main() {
1314
TestWidgetsFlutterBinding.ensureInitialized();
15+
late MockStreamingCameraPlatform mockPlatform;
1416

1517
setUp(() {
16-
CameraPlatform.instance = MockCameraPlatform();
18+
mockPlatform = MockStreamingCameraPlatform();
19+
CameraPlatform.instance = mockPlatform;
1720
});
1821

1922
test('startImageStream() throws $CameraException when uninitialized', () {
@@ -87,13 +90,6 @@ void main() {
8790
});
8891

8992
test('startImageStream() calls CameraPlatform', () async {
90-
final MethodChannelMock cameraChannelMock = MethodChannelMock(
91-
channelName: 'plugins.flutter.io/camera',
92-
methods: <String, dynamic>{'startImageStream': <String, dynamic>{}});
93-
final MethodChannelMock streamChannelMock = MethodChannelMock(
94-
channelName: 'plugins.flutter.io/camera/imageStream',
95-
methods: <String, dynamic>{'listen': <String, dynamic>{}});
96-
9793
final CameraController cameraController = CameraController(
9894
const CameraDescription(
9995
name: 'cam',
@@ -104,10 +100,8 @@ void main() {
104100

105101
await cameraController.startImageStream((CameraImage image) => null);
106102

107-
expect(cameraChannelMock.log,
108-
<Matcher>[isMethodCall('startImageStream', arguments: null)]);
109-
expect(streamChannelMock.log,
110-
<Matcher>[isMethodCall('listen', arguments: null)]);
103+
expect(mockPlatform.streamCallLog,
104+
<String>['onStreamedFrameAvailable', 'listen']);
111105
});
112106

113107
test('stopImageStream() throws $CameraException when uninitialized', () {
@@ -178,19 +172,6 @@ void main() {
178172
});
179173

180174
test('stopImageStream() intended behaviour', () async {
181-
final MethodChannelMock cameraChannelMock = MethodChannelMock(
182-
channelName: 'plugins.flutter.io/camera',
183-
methods: <String, dynamic>{
184-
'startImageStream': <String, dynamic>{},
185-
'stopImageStream': <String, dynamic>{}
186-
});
187-
final MethodChannelMock streamChannelMock = MethodChannelMock(
188-
channelName: 'plugins.flutter.io/camera/imageStream',
189-
methods: <String, dynamic>{
190-
'listen': <String, dynamic>{},
191-
'cancel': <String, dynamic>{}
192-
});
193-
194175
final CameraController cameraController = CameraController(
195176
const CameraDescription(
196177
name: 'cam',
@@ -201,14 +182,33 @@ void main() {
201182
await cameraController.startImageStream((CameraImage image) => null);
202183
await cameraController.stopImageStream();
203184

204-
expect(cameraChannelMock.log, <Matcher>[
205-
isMethodCall('startImageStream', arguments: null),
206-
isMethodCall('stopImageStream', arguments: null)
207-
]);
208-
209-
expect(streamChannelMock.log, <Matcher>[
210-
isMethodCall('listen', arguments: null),
211-
isMethodCall('cancel', arguments: null)
212-
]);
185+
expect(mockPlatform.streamCallLog,
186+
<String>['onStreamedFrameAvailable', 'listen', 'cancel']);
213187
});
214188
}
189+
190+
class MockStreamingCameraPlatform extends MockCameraPlatform {
191+
List<String> streamCallLog = <String>[];
192+
193+
StreamController<CameraImageData>? _streamController;
194+
195+
@override
196+
Stream<CameraImageData> onStreamedFrameAvailable(int cameraId,
197+
{CameraImageStreamOptions? options}) {
198+
streamCallLog.add('onStreamedFrameAvailable');
199+
_streamController = StreamController<CameraImageData>(
200+
onListen: _onFrameStreamListen,
201+
onCancel: _onFrameStreamCancel,
202+
);
203+
return _streamController!.stream;
204+
}
205+
206+
void _onFrameStreamListen() {
207+
streamCallLog.add('listen');
208+
}
209+
210+
FutureOr<void> _onFrameStreamCancel() async {
211+
streamCallLog.add('cancel');
212+
_streamController = null;
213+
}
214+
}

packages/camera/camera/test/camera_image_test.dart

+54-1
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,64 @@
55
import 'dart:typed_data';
66

77
import 'package:camera/camera.dart';
8+
import 'package:camera_platform_interface/camera_platform_interface.dart';
89
import 'package:flutter/foundation.dart';
910
import 'package:flutter_test/flutter_test.dart';
1011

1112
void main() {
12-
group('$CameraImage tests', () {
13+
test('translates correctly from platform interface classes', () {
14+
final CameraImageData originalImage = CameraImageData(
15+
format: const CameraImageFormat(ImageFormatGroup.jpeg, raw: 1234),
16+
planes: <CameraImagePlane>[
17+
CameraImagePlane(
18+
bytes: Uint8List.fromList(<int>[1, 2, 3, 4]),
19+
bytesPerRow: 20,
20+
bytesPerPixel: 3,
21+
width: 200,
22+
height: 100,
23+
),
24+
CameraImagePlane(
25+
bytes: Uint8List.fromList(<int>[5, 6, 7, 8]),
26+
bytesPerRow: 18,
27+
bytesPerPixel: 4,
28+
width: 220,
29+
height: 110,
30+
),
31+
],
32+
width: 640,
33+
height: 480,
34+
lensAperture: 2.5,
35+
sensorExposureTime: 5,
36+
sensorSensitivity: 1.3,
37+
);
38+
39+
final CameraImage image = CameraImage.fromPlatformInterface(originalImage);
40+
// Simple values.
41+
expect(image.width, 640);
42+
expect(image.height, 480);
43+
expect(image.lensAperture, 2.5);
44+
expect(image.sensorExposureTime, 5);
45+
expect(image.sensorSensitivity, 1.3);
46+
// Format.
47+
expect(image.format.group, ImageFormatGroup.jpeg);
48+
expect(image.format.raw, 1234);
49+
// Planes.
50+
expect(image.planes.length, originalImage.planes.length);
51+
for (int i = 0; i < image.planes.length; i++) {
52+
expect(
53+
image.planes[i].bytes.length, originalImage.planes[i].bytes.length);
54+
for (int j = 0; j < image.planes[i].bytes.length; j++) {
55+
expect(image.planes[i].bytes[j], originalImage.planes[i].bytes[j]);
56+
}
57+
expect(
58+
image.planes[i].bytesPerPixel, originalImage.planes[i].bytesPerPixel);
59+
expect(image.planes[i].bytesPerRow, originalImage.planes[i].bytesPerRow);
60+
expect(image.planes[i].width, originalImage.planes[i].width);
61+
expect(image.planes[i].height, originalImage.planes[i].height);
62+
}
63+
});
64+
65+
group('legacy constructors', () {
1366
test('$CameraImage can be created', () {
1467
debugDefaultTargetPlatformOverride = TargetPlatform.android;
1568
final CameraImage cameraImage =

packages/camera/camera/test/utils/method_channel_mock.dart

-39
This file was deleted.

0 commit comments

Comments
 (0)