From af89cf427d0a4f0b8e6f075b1a7ee678ddfafdf6 Mon Sep 17 00:00:00 2001 From: Adam Harwood Date: Thu, 8 Dec 2022 10:18:56 +1000 Subject: [PATCH 1/3] Re-enable stream and record This re-commits the content from https://github.com/flutter/plugins/pull/6290. Will make a subsequent commit to try and fix the broken integ tests. --- packages/camera/camera_android/CHANGELOG.md | 4 + .../io/flutter/plugins/camera/Camera.java | 96 ++++++++++++------- .../plugins/camera/MethodCallHandlerImpl.java | 5 +- .../example/integration_test/camera_test.dart | 40 ++++++++ .../example/lib/camera_controller.dart | 12 ++- .../camera_android/example/pubspec.yaml | 2 +- .../lib/src/android_camera.dart | 30 +++++- packages/camera/camera_android/pubspec.yaml | 4 +- .../test/android_camera_test.dart | 29 +++++- .../camera/camera_avfoundation/CHANGELOG.md | 4 + .../example/integration_test/camera_test.dart | 29 ++++++ .../example/lib/camera_controller.dart | 11 ++- .../ios/Classes/CameraPlugin.m | 7 +- .../camera_avfoundation/ios/Classes/FLTCam.h | 9 ++ .../camera_avfoundation/ios/Classes/FLTCam.m | 9 ++ .../lib/src/avfoundation_camera.dart | 33 ++++++- .../camera/camera_avfoundation/pubspec.yaml | 4 +- .../test/avfoundation_camera_test.dart | 27 +++++- .../camera_platform_interface/CHANGELOG.md | 4 + .../method_channel/method_channel_camera.dart | 30 +++++- .../camera_platform_interface/pubspec.yaml | 2 +- .../method_channel_camera_test.dart | 4 +- packages/camera/camera_web/CHANGELOG.md | 4 + .../integration_test/camera_web_test.dart | 27 ++++++ .../camera/camera_web/lib/src/camera_web.dart | 18 +++- packages/camera/camera_web/pubspec.yaml | 4 +- packages/camera/camera_windows/CHANGELOG.md | 4 + .../camera_windows/lib/camera_windows.dart | 21 ++-- packages/camera/camera_windows/pubspec.yaml | 4 +- .../test/camera_windows_test.dart | 9 ++ 30 files changed, 410 insertions(+), 76 deletions(-) diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md index 750145ea1cb4..ef066c8e56bf 100644 --- a/packages/camera/camera_android/CHANGELOG.md +++ b/packages/camera/camera_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.1 + +* Implements option to also stream when recording a video. + ## 0.10.0+5 * Fixes `ArrayIndexOutOfBoundsException` when the permission request is interrupted. diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java index 3d2df98b60da..7c592b9c7e99 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -522,6 +522,21 @@ private void refreshPreviewCaptureSession( } } + private void startCapture(boolean record, boolean stream) throws CameraAccessException { + List surfaces = new ArrayList<>(); + Runnable successCallback = null; + if (record) { + surfaces.add(mediaRecorder.getSurface()); + successCallback = () -> mediaRecorder.start(); + } + if (stream) { + surfaces.add(imageStreamReader.getSurface()); + } + + createCaptureSession( + CameraDevice.TEMPLATE_RECORD, successCallback, surfaces.toArray(new Surface[0])); + } + public void takePicture(@NonNull final Result result) { // Only take one picture at a time. if (cameraCaptureCallback.getCameraState() != CameraState.STATE_PREVIEW) { @@ -731,29 +746,17 @@ private void unlockAutoFocus() { dartMessenger.error(flutterResult, errorCode, errorMessage, null)); } - public void startVideoRecording(@NonNull Result result) { - final File outputDir = applicationContext.getCacheDir(); - try { - captureFile = File.createTempFile("REC", ".mp4", outputDir); - } catch (IOException | SecurityException e) { - result.error("cannotCreateFile", e.getMessage(), null); - return; - } - try { - prepareMediaRecorder(captureFile.getAbsolutePath()); - } catch (IOException e) { - recordingVideo = false; - captureFile = null; - result.error("videoRecordingFailed", e.getMessage(), null); - return; + public void startVideoRecording( + @NonNull Result result, @Nullable EventChannel imageStreamChannel) { + prepareRecording(result); + + if (imageStreamChannel != null) { + setStreamHandler(imageStreamChannel); } - // Re-create autofocus feature so it's using video focus mode now. - cameraFeatures.setAutoFocus( - cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true)); + recordingVideo = true; try { - createCaptureSession( - CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); + startCapture(true, imageStreamChannel != null); result.success(null); } catch (CameraAccessException e) { recordingVideo = false; @@ -1073,21 +1076,10 @@ public void startPreview() throws CameraAccessException { public void startPreviewWithImageStream(EventChannel imageStreamChannel) throws CameraAccessException { - createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); - Log.i(TAG, "startPreviewWithImageStream"); - - imageStreamChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink imageStreamSink) { - setImageStreamImageAvailableListener(imageStreamSink); - } + setStreamHandler(imageStreamChannel); - @Override - public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, backgroundHandler); - } - }); + startCapture(false, true); + Log.i(TAG, "startPreviewWithImageStream"); } /** @@ -1117,6 +1109,42 @@ public void onError(String errorCode, String errorMessage) { cameraCaptureCallback.setCameraState(CameraState.STATE_PREVIEW); } + private void prepareRecording(@NonNull Result result) { + final File outputDir = applicationContext.getCacheDir(); + try { + captureFile = File.createTempFile("REC", ".mp4", outputDir); + } catch (IOException | SecurityException e) { + result.error("cannotCreateFile", e.getMessage(), null); + return; + } + try { + prepareMediaRecorder(captureFile.getAbsolutePath()); + } catch (IOException e) { + recordingVideo = false; + captureFile = null; + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + // Re-create autofocus feature so it's using video focus mode now. + cameraFeatures.setAutoFocus( + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true)); + } + + private void setStreamHandler(EventChannel imageStreamChannel) { + imageStreamChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink imageStreamSink) { + setImageStreamImageAvailableListener(imageStreamSink); + } + + @Override + public void onCancel(Object o) { + imageStreamReader.setOnImageAvailableListener(null, backgroundHandler); + } + }); + } + private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { imageStreamReader.setOnImageAvailableListener( reader -> { diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 38201e1136c9..432344ade8cd 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -26,6 +26,7 @@ import io.flutter.view.TextureRegistry; import java.util.HashMap; import java.util.Map; +import java.util.Objects; final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { private final Activity activity; @@ -118,7 +119,9 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) } case "startVideoRecording": { - camera.startVideoRecording(result); + camera.startVideoRecording( + result, + Objects.equals(call.argument("enableStream"), true) ? imageStreamChannel : null); break; } case "stopVideoRecording": diff --git a/packages/camera/camera_android/example/integration_test/camera_test.dart b/packages/camera/camera_android/example/integration_test/camera_test.dart index 3b1aae6aec51..4c51a593f259 100644 --- a/packages/camera/camera_android/example/integration_test/camera_test.dart +++ b/packages/camera/camera_android/example/integration_test/camera_test.dart @@ -245,4 +245,44 @@ void main() { await controller.dispose(); }, ); + + testWidgets( + 'recording with image stream', + (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.isEmpty) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + bool isDetecting = false; + + await controller.startVideoRecording( + streamCallback: (CameraImageData image) { + if (isDetecting) { + return; + } + + isDetecting = true; + + expectLater(image, isNotNull).whenComplete(() => isDetecting = false); + }); + + expect(controller.value.isStreamingImages, true); + + sleep(const Duration(milliseconds: 500)); + + await controller.stopVideoRecording(); + await controller.dispose(); + + expect(controller.value.isStreamingImages, false); + }, + ); } diff --git a/packages/camera/camera_android/example/lib/camera_controller.dart b/packages/camera/camera_android/example/lib/camera_controller.dart index 09441cc5449c..94d8ef6540b1 100644 --- a/packages/camera/camera_android/example/lib/camera_controller.dart +++ b/packages/camera/camera_android/example/lib/camera_controller.dart @@ -306,11 +306,14 @@ class CameraController extends ValueNotifier { /// /// The video is returned as a [XFile] after calling [stopVideoRecording]. /// Throws a [CameraException] if the capture fails. - Future startVideoRecording() async { - await CameraPlatform.instance.startVideoRecording(_cameraId); + Future startVideoRecording( + {Function(CameraImageData image)? streamCallback}) async { + await CameraPlatform.instance.startVideoCapturing( + VideoCaptureOptions(_cameraId, streamCallback: streamCallback)); value = value.copyWith( isRecordingVideo: true, isRecordingPaused: false, + isStreamingImages: streamCallback != null, recordingOrientation: Optional.of( value.lockedCaptureOrientation ?? value.deviceOrientation)); } @@ -319,10 +322,15 @@ class CameraController extends ValueNotifier { /// /// Throws a [CameraException] if the capture failed. Future stopVideoRecording() async { + if (value.isStreamingImages) { + await stopImageStream(); + } + final XFile file = await CameraPlatform.instance.stopVideoRecording(_cameraId); value = value.copyWith( isRecordingVideo: false, + isRecordingPaused: false, recordingOrientation: const Optional.absent(), ); return file; diff --git a/packages/camera/camera_android/example/pubspec.yaml b/packages/camera/camera_android/example/pubspec.yaml index 2e530e02ca71..8c985d94fd5a 100644 --- a/packages/camera/camera_android/example/pubspec.yaml +++ b/packages/camera/camera_android/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - camera_platform_interface: ^2.2.0 + camera_platform_interface: ^2.3.1 flutter: sdk: flutter path_provider: ^2.0.0 diff --git a/packages/camera/camera_android/lib/src/android_camera.dart b/packages/camera/camera_android/lib/src/android_camera.dart index 36077eac8eed..4b342eee08d5 100644 --- a/packages/camera/camera_android/lib/src/android_camera.dart +++ b/packages/camera/camera_android/lib/src/android_camera.dart @@ -248,13 +248,25 @@ class AndroidCamera extends CameraPlatform { @override Future startVideoRecording(int cameraId, {Duration? maxVideoDuration}) async { + return startVideoCapturing( + VideoCaptureOptions(cameraId, maxDuration: maxVideoDuration)); + } + + @override + Future startVideoCapturing(VideoCaptureOptions options) async { await _channel.invokeMethod( 'startVideoRecording', { - 'cameraId': cameraId, - 'maxVideoDuration': maxVideoDuration?.inMilliseconds, + 'cameraId': options.cameraId, + 'maxVideoDuration': options.maxDuration?.inMilliseconds, + 'enableStream': options.streamCallback != null, }, ); + + if (options.streamCallback != null) { + _installStreamController().stream.listen(options.streamCallback); + _startStreamListener(); + } } @override @@ -290,13 +302,19 @@ class AndroidCamera extends CameraPlatform { @override Stream onStreamedFrameAvailable(int cameraId, {CameraImageStreamOptions? options}) { + _installStreamController(onListen: _onFrameStreamListen); + return _frameStreamController!.stream; + } + + StreamController _installStreamController( + {Function()? onListen}) { _frameStreamController = StreamController( - onListen: _onFrameStreamListen, + onListen: onListen ?? () {}, onPause: _onFrameStreamPauseResume, onResume: _onFrameStreamPauseResume, onCancel: _onFrameStreamCancel, ); - return _frameStreamController!.stream; + return _frameStreamController!; } void _onFrameStreamListen() { @@ -305,6 +323,10 @@ class AndroidCamera extends CameraPlatform { Future _startPlatformStream() async { await _channel.invokeMethod('startImageStream'); + _startStreamListener(); + } + + void _startStreamListener() { const EventChannel cameraEventChannel = EventChannel('plugins.flutter.io/camera_android/imageStream'); _platformImageStreamSubscription = diff --git a/packages/camera/camera_android/pubspec.yaml b/packages/camera/camera_android/pubspec.yaml index 6aa78268cb00..7ed5077c315e 100644 --- a/packages/camera/camera_android/pubspec.yaml +++ b/packages/camera/camera_android/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android description: Android implementation of the camera plugin. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.10.0+5 +version: 0.10.1 environment: sdk: ">=2.14.0 <3.0.0" @@ -18,7 +18,7 @@ flutter: dartPluginClass: AndroidCamera dependencies: - camera_platform_interface: ^2.2.0 + camera_platform_interface: ^2.3.1 flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.2 diff --git a/packages/camera/camera_android/test/android_camera_test.dart b/packages/camera/camera_android/test/android_camera_test.dart index 3e50e6918648..e35d0fd1beb4 100644 --- a/packages/camera/camera_android/test/android_camera_test.dart +++ b/packages/camera/camera_android/test/android_camera_test.dart @@ -587,6 +587,7 @@ void main() { isMethodCall('startVideoRecording', arguments: { 'cameraId': cameraId, 'maxVideoDuration': null, + 'enableStream': false, }), ]); }); @@ -609,7 +610,33 @@ void main() { expect(channel.log, [ isMethodCall('startVideoRecording', arguments: { 'cameraId': cameraId, - 'maxVideoDuration': 10000 + 'maxVideoDuration': 10000, + 'enableStream': false, + }), + ]); + }); + + test( + 'Should pass enableStream if callback is passed when starting recording a video', + () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'startVideoRecording': null}, + ); + + // Act + await camera.startVideoCapturing( + VideoCaptureOptions(cameraId, + streamCallback: (CameraImageData imageData) {}), + ); + + // Assert + expect(channel.log, [ + isMethodCall('startVideoRecording', arguments: { + 'cameraId': cameraId, + 'maxVideoDuration': null, + 'enableStream': true, }), ]); }); diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index 12d9a53ea248..641272af2246 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.9 + +* Implements option to also stream when recording a video. + ## 0.9.8+6 * Updates code for `no_leading_underscores_for_local_identifiers` lint. diff --git a/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart b/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart index 3e62edc2c495..2b23d82e619f 100644 --- a/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart +++ b/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart @@ -253,4 +253,33 @@ void main() { expect(image.planes.length, 1); }, ); + + testWidgets('Recording with video streaming', (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.isEmpty) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + await controller.prepareForVideoRecording(); + final Completer completer = Completer(); + await controller.startVideoRecording( + streamCallback: (CameraImageData image) { + if (!completer.isCompleted) { + completer.complete(image); + } + }); + sleep(const Duration(milliseconds: 500)); + await controller.stopVideoRecording(); + await controller.dispose(); + + expect(await completer.future, isNotNull); + }); } diff --git a/packages/camera/camera_avfoundation/example/lib/camera_controller.dart b/packages/camera/camera_avfoundation/example/lib/camera_controller.dart index 09441cc5449c..2fcd868934fb 100644 --- a/packages/camera/camera_avfoundation/example/lib/camera_controller.dart +++ b/packages/camera/camera_avfoundation/example/lib/camera_controller.dart @@ -306,11 +306,14 @@ class CameraController extends ValueNotifier { /// /// The video is returned as a [XFile] after calling [stopVideoRecording]. /// Throws a [CameraException] if the capture fails. - Future startVideoRecording() async { - await CameraPlatform.instance.startVideoRecording(_cameraId); + Future startVideoRecording( + {Function(CameraImageData image)? streamCallback}) async { + await CameraPlatform.instance.startVideoCapturing( + VideoCaptureOptions(_cameraId, streamCallback: streamCallback)); value = value.copyWith( isRecordingVideo: true, isRecordingPaused: false, + isStreamingImages: streamCallback != null, recordingOrientation: Optional.of( value.lockedCaptureOrientation ?? value.deviceOrientation)); } @@ -319,6 +322,10 @@ class CameraController extends ValueNotifier { /// /// Throws a [CameraException] if the capture failed. Future stopVideoRecording() async { + if (value.isStreamingImages) { + await stopImageStream(); + } + final XFile file = await CameraPlatform.instance.stopVideoRecording(_cameraId); value = value.copyWith( diff --git a/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m index 628211ac7f7a..b85f68d1f957 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m @@ -201,7 +201,12 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call [self.camera setUpCaptureSessionForAudio]; [result sendSuccess]; } else if ([@"startVideoRecording" isEqualToString:call.method]) { - [_camera startVideoRecordingWithResult:result]; + BOOL enableStream = [call.arguments[@"enableStream"] boolValue]; + if (enableStream) { + [_camera startVideoRecordingWithResult:result messengerForStreaming:_messenger]; + } else { + [_camera startVideoRecordingWithResult:result]; + } } else if ([@"stopVideoRecording" isEqualToString:call.method]) { [_camera stopVideoRecordingWithResult:result]; } else if ([@"pauseVideoRecording" isEqualToString:call.method]) { diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h index 8a5dafaf8354..85b8e2ae06f2 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h @@ -50,6 +50,15 @@ NS_ASSUME_NONNULL_BEGIN - (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10)); - (void)close; - (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result; +/** + * Starts recording a video with an optional streaming messenger. + * If the messenger is non-null then it will be called for each + * captured frame, allowing streaming concurrently with recording. + * + * @param messenger Nullable messenger for capturing each frame. + */ +- (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result + messengerForStreaming:(nullable NSObject *)messenger; - (void)stopVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result; - (void)pauseVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result; - (void)resumeVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result; diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m index 90b81adbd84c..a7d6cd24be3c 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m @@ -623,7 +623,16 @@ - (CVPixelBufferRef)copyPixelBuffer { } - (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { + [self startVideoRecordingWithResult:result messengerForStreaming:nil]; +} + +- (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result + messengerForStreaming:(nullable NSObject *)messenger { if (!_isRecording) { + if (messenger != nil) { + [self startImageStreamWithMessenger:messenger]; + } + NSError *error; _videoRecordingPath = [self getTemporaryFilePathWithExtension:@"mp4" subfolder:@"videos" diff --git a/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart index 9bdadfb4536f..011616d2d9f4 100644 --- a/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart +++ b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart @@ -248,13 +248,26 @@ class AVFoundationCamera extends CameraPlatform { @override Future startVideoRecording(int cameraId, {Duration? maxVideoDuration}) async { + return startVideoCapturing( + VideoCaptureOptions(cameraId, maxDuration: maxVideoDuration)); + } + + @override + Future startVideoCapturing(VideoCaptureOptions options) async { await _channel.invokeMethod( 'startVideoRecording', { - 'cameraId': cameraId, - 'maxVideoDuration': maxVideoDuration?.inMilliseconds, + 'cameraId': options.cameraId, + 'maxVideoDuration': options.maxDuration?.inMilliseconds, + 'enableStream': options.streamCallback != null, }, ); + + if (options.streamCallback != null) { + _frameStreamController = _createStreamController(); + _frameStreamController!.stream.listen(options.streamCallback); + _startStreamListener(); + } } @override @@ -290,13 +303,19 @@ class AVFoundationCamera extends CameraPlatform { @override Stream onStreamedFrameAvailable(int cameraId, {CameraImageStreamOptions? options}) { - _frameStreamController = StreamController( - onListen: _onFrameStreamListen, + _frameStreamController = + _createStreamController(onListen: _onFrameStreamListen); + return _frameStreamController!.stream; + } + + StreamController _createStreamController( + {Function()? onListen}) { + return StreamController( + onListen: onListen ?? () {}, onPause: _onFrameStreamPauseResume, onResume: _onFrameStreamPauseResume, onCancel: _onFrameStreamCancel, ); - return _frameStreamController!.stream; } void _onFrameStreamListen() { @@ -305,6 +324,10 @@ class AVFoundationCamera extends CameraPlatform { Future _startPlatformStream() async { await _channel.invokeMethod('startImageStream'); + _startStreamListener(); + } + + void _startStreamListener() { const EventChannel cameraEventChannel = EventChannel('plugins.flutter.io/camera_avfoundation/imageStream'); _platformImageStreamSubscription = diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index f394d59e81d5..e60f5e406aad 100644 --- a/packages/camera/camera_avfoundation/pubspec.yaml +++ b/packages/camera/camera_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_avfoundation description: iOS implementation of the camera plugin. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.8+6 +version: 0.9.9 environment: sdk: ">=2.14.0 <3.0.0" @@ -17,7 +17,7 @@ flutter: dartPluginClass: AVFoundationCamera dependencies: - camera_platform_interface: ^2.2.0 + camera_platform_interface: ^2.3.1 flutter: sdk: flutter stream_transform: ^2.0.0 diff --git a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart index 60109a4172b7..8c5fad1fec8e 100644 --- a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart +++ b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart @@ -587,6 +587,7 @@ void main() { isMethodCall('startVideoRecording', arguments: { 'cameraId': cameraId, 'maxVideoDuration': null, + 'enableStream': false, }), ]); }); @@ -609,7 +610,31 @@ void main() { expect(channel.log, [ isMethodCall('startVideoRecording', arguments: { 'cameraId': cameraId, - 'maxVideoDuration': 10000 + 'maxVideoDuration': 10000, + 'enableStream': false, + }), + ]); + }); + + test( + 'Should pass enableStream if callback is passed when starting recording a video', + () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'startVideoRecording': null}, + ); + + // Act + await camera.startVideoCapturing(VideoCaptureOptions(cameraId, + streamCallback: (CameraImageData imageData) {})); + + // Assert + expect(channel.log, [ + isMethodCall('startVideoRecording', arguments: { + 'cameraId': cameraId, + 'maxVideoDuration': null, + 'enableStream': true, }), ]); }); diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index 9c93cf93887e..5cde03c2e0db 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.2 + +* Updates MethodChannelCamera to have startVideoRecording call the newer startVideoCapturing. + ## 2.3.1 * Exports VideoCaptureOptions to allow dependencies to implement concurrent stream and record. diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart index 37c00d64ede2..34c3fa2cca36 100644 --- a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -240,13 +240,25 @@ class MethodChannelCamera extends CameraPlatform { @override Future startVideoRecording(int cameraId, {Duration? maxVideoDuration}) async { + return startVideoCapturing( + VideoCaptureOptions(cameraId, maxDuration: maxVideoDuration)); + } + + @override + Future startVideoCapturing(VideoCaptureOptions options) async { await _channel.invokeMethod( 'startVideoRecording', { - 'cameraId': cameraId, - 'maxVideoDuration': maxVideoDuration?.inMilliseconds, + 'cameraId': options.cameraId, + 'maxVideoDuration': options.maxDuration?.inMilliseconds, + 'enableStream': options.streamCallback != null, }, ); + + if (options.streamCallback != null) { + _installStreamController().stream.listen(options.streamCallback); + _startStreamListener(); + } } @override @@ -282,13 +294,19 @@ class MethodChannelCamera extends CameraPlatform { @override Stream onStreamedFrameAvailable(int cameraId, {CameraImageStreamOptions? options}) { + _installStreamController(onListen: _onFrameStreamListen); + return _frameStreamController!.stream; + } + + StreamController _installStreamController( + {Function()? onListen}) { _frameStreamController = StreamController( - onListen: _onFrameStreamListen, + onListen: onListen ?? () {}, onPause: _onFrameStreamPauseResume, onResume: _onFrameStreamPauseResume, onCancel: _onFrameStreamCancel, ); - return _frameStreamController!.stream; + return _frameStreamController!; } void _onFrameStreamListen() { @@ -297,6 +315,10 @@ class MethodChannelCamera extends CameraPlatform { Future _startPlatformStream() async { await _channel.invokeMethod('startImageStream'); + _startStreamListener(); + } + + void _startStreamListener() { const EventChannel cameraEventChannel = EventChannel('plugins.flutter.io/camera/imageStream'); _platformImageStreamSubscription = diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index 19642a0c958e..cb21a6c7e09c 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ 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 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.3.1 +version: 2.3.2 environment: sdk: '>=2.12.0 <3.0.0' diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart index 60f42fd4af4a..a58f7b4c5841 100644 --- a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -569,6 +569,7 @@ void main() { isMethodCall('startVideoRecording', arguments: { 'cameraId': cameraId, 'maxVideoDuration': null, + 'enableStream': false, }), ]); }); @@ -591,7 +592,8 @@ void main() { expect(channel.log, [ isMethodCall('startVideoRecording', arguments: { 'cameraId': cameraId, - 'maxVideoDuration': 10000 + 'maxVideoDuration': 10000, + 'enableStream': false, }), ]); }); diff --git a/packages/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md index f4989cfd5bff..d8d0c93dde11 100644 --- a/packages/camera/camera_web/CHANGELOG.md +++ b/packages/camera/camera_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.1 + +* Updates to latest camera platform interface, and fails if user attempts to use streaming with recording (since streaming is currently unsupported on web). + ## 0.3.0+1 * Updates imports for `prefer_relative_imports`. diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 8aefea6b2172..820a84be7207 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -1191,6 +1191,33 @@ void main() { }); }); + group('startVideoCapturing', () { + late Camera camera; + + setUp(() { + camera = MockCamera(); + + when(camera.startVideoRecording).thenAnswer((Invocation _) async {}); + + when(() => camera.onVideoRecordingError) + .thenAnswer((Invocation _) => const Stream.empty()); + }); + + testWidgets('fails if trying to stream', (WidgetTester tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.startVideoCapturing(VideoCaptureOptions( + cameraId, + streamCallback: (CameraImageData imageData) {})), + throwsA( + isA(), + ), + ); + }); + }); + group('stopVideoRecording', () { testWidgets('stops a video recording', (WidgetTester tester) async { final MockCamera camera = MockCamera(); diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index d440653cd424..52fdc1c3f8d6 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -451,23 +451,33 @@ class CameraPlugin extends CameraPlatform { @override Future startVideoRecording(int cameraId, {Duration? maxVideoDuration}) { + return startVideoCapturing( + VideoCaptureOptions(cameraId, maxDuration: maxVideoDuration)); + } + + @override + Future startVideoCapturing(VideoCaptureOptions options) { + if (options.streamCallback != null || options.streamOptions != null) { + throw UnimplementedError('Streaming is not currently supported on web'); + } + try { - final Camera camera = getCamera(cameraId); + final Camera camera = getCamera(options.cameraId); // Add camera's video recording errors to the camera events stream. // The error event fires when the video recording is not allowed or an unsupported // codec is used. - _cameraVideoRecordingErrorSubscriptions[cameraId] = + _cameraVideoRecordingErrorSubscriptions[options.cameraId] = camera.onVideoRecordingError.listen((html.ErrorEvent errorEvent) { cameraEventStreamController.add( CameraErrorEvent( - cameraId, + options.cameraId, 'Error code: ${errorEvent.type}, error message: ${errorEvent.message}.', ), ); }); - return camera.startVideoRecording(maxVideoDuration: maxVideoDuration); + return camera.startVideoRecording(maxVideoDuration: options.maxDuration); } on html.DomException catch (e) { throw PlatformException(code: e.name, message: e.message); } on CameraWebException catch (e) { diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index ef9c45c71796..2368e62abee6 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_web description: A Flutter plugin for getting information about and controlling the camera on Web. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.3.0+1 +version: 0.3.1 environment: sdk: ">=2.12.0 <3.0.0" @@ -17,7 +17,7 @@ flutter: fileName: camera_web.dart dependencies: - camera_platform_interface: ^2.1.0 + camera_platform_interface: ^2.3.1 flutter: sdk: flutter flutter_web_plugins: diff --git a/packages/camera/camera_windows/CHANGELOG.md b/packages/camera/camera_windows/CHANGELOG.md index 71c5d56524a6..f46bb667735f 100644 --- a/packages/camera/camera_windows/CHANGELOG.md +++ b/packages/camera/camera_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.1+3 + +* Updates to latest camera platform interface but fails if user attempts to use streaming with recording (since streaming is currently unsupported on Windows). + ## 0.2.1+2 * Updates code for `no_leading_underscores_for_local_identifiers` lint. diff --git a/packages/camera/camera_windows/lib/camera_windows.dart b/packages/camera/camera_windows/lib/camera_windows.dart index 14134479994b..79dd305e2e14 100644 --- a/packages/camera/camera_windows/lib/camera_windows.dart +++ b/packages/camera/camera_windows/lib/camera_windows.dart @@ -214,15 +214,24 @@ class CameraWindows extends CameraPlatform { pluginChannel.invokeMethod('prepareForVideoRecording'); @override - Future startVideoRecording( - int cameraId, { - Duration? maxVideoDuration, - }) async { + Future startVideoRecording(int cameraId, + {Duration? maxVideoDuration}) async { + return startVideoCapturing( + VideoCaptureOptions(cameraId, maxDuration: maxVideoDuration)); + } + + @override + Future startVideoCapturing(VideoCaptureOptions options) async { + if (options.streamCallback != null || options.streamOptions != null) { + throw UnimplementedError( + 'Streaming is not currently supported on Windows'); + } + await pluginChannel.invokeMethod( 'startVideoRecording', { - 'cameraId': cameraId, - 'maxVideoDuration': maxVideoDuration?.inMilliseconds, + 'cameraId': options.cameraId, + 'maxVideoDuration': options.maxDuration?.inMilliseconds, }, ); } diff --git a/packages/camera/camera_windows/pubspec.yaml b/packages/camera/camera_windows/pubspec.yaml index 1eab9fa108ef..523ee14d186e 100644 --- a/packages/camera/camera_windows/pubspec.yaml +++ b/packages/camera/camera_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_windows description: A Flutter plugin for getting information about and controlling the camera on Windows. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.2.1+2 +version: 0.2.1+3 environment: sdk: ">=2.12.0 <3.0.0" @@ -17,7 +17,7 @@ flutter: dartPluginClass: CameraWindows dependencies: - camera_platform_interface: ^2.1.2 + camera_platform_interface: ^2.3.1 cross_file: ^0.3.1 flutter: sdk: flutter diff --git a/packages/camera/camera_windows/test/camera_windows_test.dart b/packages/camera/camera_windows/test/camera_windows_test.dart index c1a0fe40325f..615020e9f17d 100644 --- a/packages/camera/camera_windows/test/camera_windows_test.dart +++ b/packages/camera/camera_windows/test/camera_windows_test.dart @@ -447,6 +447,15 @@ void main() { ]); }); + test('capturing fails if trying to stream', () async { + // Act and Assert + expect( + () => plugin.startVideoCapturing(VideoCaptureOptions(cameraId, + streamCallback: (CameraImageData imageData) {})), + throwsA(isA()), + ); + }); + test('Should stop a video recording and return the file', () async { // Arrange final MethodChannelMock channel = MethodChannelMock( From 4b844850fe32d785ed182ba4c08e0bc395ca8db1 Mon Sep 17 00:00:00 2001 From: Adam Harwood Date: Thu, 8 Dec 2022 10:23:09 +1000 Subject: [PATCH 2/3] 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. --- .../camera_android/example/integration_test/camera_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera_android/example/integration_test/camera_test.dart b/packages/camera/camera_android/example/integration_test/camera_test.dart index 4c51a593f259..4e75fdc9ef57 100644 --- a/packages/camera/camera_android/example/integration_test/camera_test.dart +++ b/packages/camera/camera_android/example/integration_test/camera_test.dart @@ -272,7 +272,7 @@ void main() { isDetecting = true; - expectLater(image, isNotNull).whenComplete(() => isDetecting = false); + expectLater(image, isNotNull); }); expect(controller.value.isStreamingImages, true); From 2f4c8366eb8713a0fd335c9d9d7f122a18d611f5 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 8 Dec 2022 09:32:16 -0500 Subject: [PATCH 3/3] Trivial CHANGELOG change to trigger full CI tests --- packages/camera/camera_android/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md index ef066c8e56bf..80f03df235a2 100644 --- a/packages/camera/camera_android/CHANGELOG.md +++ b/packages/camera/camera_android/CHANGELOG.md @@ -1,6 +1,6 @@ ## 0.10.1 -* Implements option to also stream when recording a video. +* Implements an option to also stream when recording a video. ## 0.10.0+5