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

Commit 929c9a6

Browse files
[camera] Re-enable ability to concurrently record and stream video (#6808)
* Re-enable stream and record This re-commits the content from #6290. Will make a subsequent commit to try and fix the broken integ tests. * Fix broken integration test for streaming The `whenComplete` call was sometimes causing a race condition. It is also isn't needed for the test (to reset the value of isDetecting, given it's local), so removing it makes the test reliable. * Trivial CHANGELOG change to trigger full CI tests Co-authored-by: stuartmorgan <[email protected]>
1 parent 51434ec commit 929c9a6

File tree

30 files changed

+410
-76
lines changed

30 files changed

+410
-76
lines changed

packages/camera/camera_android/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.10.1
2+
3+
* Implements an option to also stream when recording a video.
4+
15
## 0.10.0+5
26

37
* Fixes `ArrayIndexOutOfBoundsException` when the permission request is interrupted.

packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java

+62-34
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,21 @@ private void refreshPreviewCaptureSession(
522522
}
523523
}
524524

525+
private void startCapture(boolean record, boolean stream) throws CameraAccessException {
526+
List<Surface> surfaces = new ArrayList<>();
527+
Runnable successCallback = null;
528+
if (record) {
529+
surfaces.add(mediaRecorder.getSurface());
530+
successCallback = () -> mediaRecorder.start();
531+
}
532+
if (stream) {
533+
surfaces.add(imageStreamReader.getSurface());
534+
}
535+
536+
createCaptureSession(
537+
CameraDevice.TEMPLATE_RECORD, successCallback, surfaces.toArray(new Surface[0]));
538+
}
539+
525540
public void takePicture(@NonNull final Result result) {
526541
// Only take one picture at a time.
527542
if (cameraCaptureCallback.getCameraState() != CameraState.STATE_PREVIEW) {
@@ -731,29 +746,17 @@ private void unlockAutoFocus() {
731746
dartMessenger.error(flutterResult, errorCode, errorMessage, null));
732747
}
733748

734-
public void startVideoRecording(@NonNull Result result) {
735-
final File outputDir = applicationContext.getCacheDir();
736-
try {
737-
captureFile = File.createTempFile("REC", ".mp4", outputDir);
738-
} catch (IOException | SecurityException e) {
739-
result.error("cannotCreateFile", e.getMessage(), null);
740-
return;
741-
}
742-
try {
743-
prepareMediaRecorder(captureFile.getAbsolutePath());
744-
} catch (IOException e) {
745-
recordingVideo = false;
746-
captureFile = null;
747-
result.error("videoRecordingFailed", e.getMessage(), null);
748-
return;
749+
public void startVideoRecording(
750+
@NonNull Result result, @Nullable EventChannel imageStreamChannel) {
751+
prepareRecording(result);
752+
753+
if (imageStreamChannel != null) {
754+
setStreamHandler(imageStreamChannel);
749755
}
750-
// Re-create autofocus feature so it's using video focus mode now.
751-
cameraFeatures.setAutoFocus(
752-
cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true));
756+
753757
recordingVideo = true;
754758
try {
755-
createCaptureSession(
756-
CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface());
759+
startCapture(true, imageStreamChannel != null);
757760
result.success(null);
758761
} catch (CameraAccessException e) {
759762
recordingVideo = false;
@@ -1073,21 +1076,10 @@ public void startPreview() throws CameraAccessException {
10731076

10741077
public void startPreviewWithImageStream(EventChannel imageStreamChannel)
10751078
throws CameraAccessException {
1076-
createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface());
1077-
Log.i(TAG, "startPreviewWithImageStream");
1078-
1079-
imageStreamChannel.setStreamHandler(
1080-
new EventChannel.StreamHandler() {
1081-
@Override
1082-
public void onListen(Object o, EventChannel.EventSink imageStreamSink) {
1083-
setImageStreamImageAvailableListener(imageStreamSink);
1084-
}
1079+
setStreamHandler(imageStreamChannel);
10851080

1086-
@Override
1087-
public void onCancel(Object o) {
1088-
imageStreamReader.setOnImageAvailableListener(null, backgroundHandler);
1089-
}
1090-
});
1081+
startCapture(false, true);
1082+
Log.i(TAG, "startPreviewWithImageStream");
10911083
}
10921084

10931085
/**
@@ -1117,6 +1109,42 @@ public void onError(String errorCode, String errorMessage) {
11171109
cameraCaptureCallback.setCameraState(CameraState.STATE_PREVIEW);
11181110
}
11191111

1112+
private void prepareRecording(@NonNull Result result) {
1113+
final File outputDir = applicationContext.getCacheDir();
1114+
try {
1115+
captureFile = File.createTempFile("REC", ".mp4", outputDir);
1116+
} catch (IOException | SecurityException e) {
1117+
result.error("cannotCreateFile", e.getMessage(), null);
1118+
return;
1119+
}
1120+
try {
1121+
prepareMediaRecorder(captureFile.getAbsolutePath());
1122+
} catch (IOException e) {
1123+
recordingVideo = false;
1124+
captureFile = null;
1125+
result.error("videoRecordingFailed", e.getMessage(), null);
1126+
return;
1127+
}
1128+
// Re-create autofocus feature so it's using video focus mode now.
1129+
cameraFeatures.setAutoFocus(
1130+
cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true));
1131+
}
1132+
1133+
private void setStreamHandler(EventChannel imageStreamChannel) {
1134+
imageStreamChannel.setStreamHandler(
1135+
new EventChannel.StreamHandler() {
1136+
@Override
1137+
public void onListen(Object o, EventChannel.EventSink imageStreamSink) {
1138+
setImageStreamImageAvailableListener(imageStreamSink);
1139+
}
1140+
1141+
@Override
1142+
public void onCancel(Object o) {
1143+
imageStreamReader.setOnImageAvailableListener(null, backgroundHandler);
1144+
}
1145+
});
1146+
}
1147+
11201148
private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) {
11211149
imageStreamReader.setOnImageAvailableListener(
11221150
reader -> {

packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import io.flutter.view.TextureRegistry;
2727
import java.util.HashMap;
2828
import java.util.Map;
29+
import java.util.Objects;
2930

3031
final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler {
3132
private final Activity activity;
@@ -118,7 +119,9 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
118119
}
119120
case "startVideoRecording":
120121
{
121-
camera.startVideoRecording(result);
122+
camera.startVideoRecording(
123+
result,
124+
Objects.equals(call.argument("enableStream"), true) ? imageStreamChannel : null);
122125
break;
123126
}
124127
case "stopVideoRecording":

packages/camera/camera_android/example/integration_test/camera_test.dart

+40
Original file line numberDiff line numberDiff line change
@@ -245,4 +245,44 @@ void main() {
245245
await controller.dispose();
246246
},
247247
);
248+
249+
testWidgets(
250+
'recording with image stream',
251+
(WidgetTester tester) async {
252+
final List<CameraDescription> cameras =
253+
await CameraPlatform.instance.availableCameras();
254+
if (cameras.isEmpty) {
255+
return;
256+
}
257+
258+
final CameraController controller = CameraController(
259+
cameras[0],
260+
ResolutionPreset.low,
261+
enableAudio: false,
262+
);
263+
264+
await controller.initialize();
265+
bool isDetecting = false;
266+
267+
await controller.startVideoRecording(
268+
streamCallback: (CameraImageData image) {
269+
if (isDetecting) {
270+
return;
271+
}
272+
273+
isDetecting = true;
274+
275+
expectLater(image, isNotNull);
276+
});
277+
278+
expect(controller.value.isStreamingImages, true);
279+
280+
sleep(const Duration(milliseconds: 500));
281+
282+
await controller.stopVideoRecording();
283+
await controller.dispose();
284+
285+
expect(controller.value.isStreamingImages, false);
286+
},
287+
);
248288
}

packages/camera/camera_android/example/lib/camera_controller.dart

+10-2
Original file line numberDiff line numberDiff line change
@@ -306,11 +306,14 @@ class CameraController extends ValueNotifier<CameraValue> {
306306
///
307307
/// The video is returned as a [XFile] after calling [stopVideoRecording].
308308
/// Throws a [CameraException] if the capture fails.
309-
Future<void> startVideoRecording() async {
310-
await CameraPlatform.instance.startVideoRecording(_cameraId);
309+
Future<void> startVideoRecording(
310+
{Function(CameraImageData image)? streamCallback}) async {
311+
await CameraPlatform.instance.startVideoCapturing(
312+
VideoCaptureOptions(_cameraId, streamCallback: streamCallback));
311313
value = value.copyWith(
312314
isRecordingVideo: true,
313315
isRecordingPaused: false,
316+
isStreamingImages: streamCallback != null,
314317
recordingOrientation: Optional<DeviceOrientation>.of(
315318
value.lockedCaptureOrientation ?? value.deviceOrientation));
316319
}
@@ -319,10 +322,15 @@ class CameraController extends ValueNotifier<CameraValue> {
319322
///
320323
/// Throws a [CameraException] if the capture failed.
321324
Future<XFile> stopVideoRecording() async {
325+
if (value.isStreamingImages) {
326+
await stopImageStream();
327+
}
328+
322329
final XFile file =
323330
await CameraPlatform.instance.stopVideoRecording(_cameraId);
324331
value = value.copyWith(
325332
isRecordingVideo: false,
333+
isRecordingPaused: false,
326334
recordingOrientation: const Optional<DeviceOrientation>.absent(),
327335
);
328336
return file;

packages/camera/camera_android/example/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ dependencies:
1414
# The example app is bundled with the plugin so we use a path dependency on
1515
# the parent directory to use the current plugin's version.
1616
path: ../
17-
camera_platform_interface: ^2.2.0
17+
camera_platform_interface: ^2.3.1
1818
flutter:
1919
sdk: flutter
2020
path_provider: ^2.0.0

packages/camera/camera_android/lib/src/android_camera.dart

+26-4
Original file line numberDiff line numberDiff line change
@@ -248,13 +248,25 @@ class AndroidCamera extends CameraPlatform {
248248
@override
249249
Future<void> startVideoRecording(int cameraId,
250250
{Duration? maxVideoDuration}) async {
251+
return startVideoCapturing(
252+
VideoCaptureOptions(cameraId, maxDuration: maxVideoDuration));
253+
}
254+
255+
@override
256+
Future<void> startVideoCapturing(VideoCaptureOptions options) async {
251257
await _channel.invokeMethod<void>(
252258
'startVideoRecording',
253259
<String, dynamic>{
254-
'cameraId': cameraId,
255-
'maxVideoDuration': maxVideoDuration?.inMilliseconds,
260+
'cameraId': options.cameraId,
261+
'maxVideoDuration': options.maxDuration?.inMilliseconds,
262+
'enableStream': options.streamCallback != null,
256263
},
257264
);
265+
266+
if (options.streamCallback != null) {
267+
_installStreamController().stream.listen(options.streamCallback);
268+
_startStreamListener();
269+
}
258270
}
259271

260272
@override
@@ -290,13 +302,19 @@ class AndroidCamera extends CameraPlatform {
290302
@override
291303
Stream<CameraImageData> onStreamedFrameAvailable(int cameraId,
292304
{CameraImageStreamOptions? options}) {
305+
_installStreamController(onListen: _onFrameStreamListen);
306+
return _frameStreamController!.stream;
307+
}
308+
309+
StreamController<CameraImageData> _installStreamController(
310+
{Function()? onListen}) {
293311
_frameStreamController = StreamController<CameraImageData>(
294-
onListen: _onFrameStreamListen,
312+
onListen: onListen ?? () {},
295313
onPause: _onFrameStreamPauseResume,
296314
onResume: _onFrameStreamPauseResume,
297315
onCancel: _onFrameStreamCancel,
298316
);
299-
return _frameStreamController!.stream;
317+
return _frameStreamController!;
300318
}
301319

302320
void _onFrameStreamListen() {
@@ -305,6 +323,10 @@ class AndroidCamera extends CameraPlatform {
305323

306324
Future<void> _startPlatformStream() async {
307325
await _channel.invokeMethod<void>('startImageStream');
326+
_startStreamListener();
327+
}
328+
329+
void _startStreamListener() {
308330
const EventChannel cameraEventChannel =
309331
EventChannel('plugins.flutter.io/camera_android/imageStream');
310332
_platformImageStreamSubscription =

packages/camera/camera_android/pubspec.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: camera_android
22
description: Android implementation of the camera plugin.
33
repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_android
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
5-
version: 0.10.0+5
5+
version: 0.10.1
66

77
environment:
88
sdk: ">=2.14.0 <3.0.0"
@@ -18,7 +18,7 @@ flutter:
1818
dartPluginClass: AndroidCamera
1919

2020
dependencies:
21-
camera_platform_interface: ^2.2.0
21+
camera_platform_interface: ^2.3.1
2222
flutter:
2323
sdk: flutter
2424
flutter_plugin_android_lifecycle: ^2.0.2

packages/camera/camera_android/test/android_camera_test.dart

+28-1
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ void main() {
587587
isMethodCall('startVideoRecording', arguments: <String, Object?>{
588588
'cameraId': cameraId,
589589
'maxVideoDuration': null,
590+
'enableStream': false,
590591
}),
591592
]);
592593
});
@@ -609,7 +610,33 @@ void main() {
609610
expect(channel.log, <Matcher>[
610611
isMethodCall('startVideoRecording', arguments: <String, Object?>{
611612
'cameraId': cameraId,
612-
'maxVideoDuration': 10000
613+
'maxVideoDuration': 10000,
614+
'enableStream': false,
615+
}),
616+
]);
617+
});
618+
619+
test(
620+
'Should pass enableStream if callback is passed when starting recording a video',
621+
() async {
622+
// Arrange
623+
final MethodChannelMock channel = MethodChannelMock(
624+
channelName: _channelName,
625+
methods: <String, dynamic>{'startVideoRecording': null},
626+
);
627+
628+
// Act
629+
await camera.startVideoCapturing(
630+
VideoCaptureOptions(cameraId,
631+
streamCallback: (CameraImageData imageData) {}),
632+
);
633+
634+
// Assert
635+
expect(channel.log, <Matcher>[
636+
isMethodCall('startVideoRecording', arguments: <String, Object?>{
637+
'cameraId': cameraId,
638+
'maxVideoDuration': null,
639+
'enableStream': true,
613640
}),
614641
]);
615642
});

packages/camera/camera_avfoundation/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.9
2+
3+
* Implements option to also stream when recording a video.
4+
15
## 0.9.8+6
26

37
* Updates code for `no_leading_underscores_for_local_identifiers` lint.

0 commit comments

Comments
 (0)