diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 84c7559d391b..d19a528a769e 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.2 + +* Implements option to also stream when recording a video. + ## 0.10.1 * Remove usage of deprecated quiver Optional type. diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index b05e61bef9f8..b201074f3810 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -452,12 +452,6 @@ class CameraController extends ValueNotifier { assert(defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS); _throwIfNotInitialized('stopImageStream'); - if (value.isRecordingVideo) { - throw CameraException( - 'A video recording is already started.', - 'stopImageStream was called while a video is being recorded.', - ); - } if (!value.isStreamingImages) { throw CameraException( 'No camera is streaming images', @@ -476,9 +470,13 @@ class CameraController extends ValueNotifier { /// Start a video recording. /// + /// You may optionally pass an [onAvailable] callback to also have the + /// video frames streamed to this callback. + /// /// The video is returned as a [XFile] after calling [stopVideoRecording]. /// Throws a [CameraException] if the capture fails. - Future startVideoRecording() async { + Future startVideoRecording( + {onLatestImageAvailable? onAvailable}) async { _throwIfNotInitialized('startVideoRecording'); if (value.isRecordingVideo) { throw CameraException( @@ -486,18 +484,21 @@ class CameraController extends ValueNotifier { 'startVideoRecording was called when a recording is already started.', ); } - if (value.isStreamingImages) { - throw CameraException( - 'A camera has started streaming images.', - 'startVideoRecording was called while a camera was streaming images.', - ); + + Function(CameraImageData image)? streamCallback; + if (onAvailable != null) { + streamCallback = (CameraImageData imageData) { + onAvailable(CameraImage.fromPlatformInterface(imageData)); + }; } try { - await CameraPlatform.instance.startVideoRecording(_cameraId); + await CameraPlatform.instance.startVideoCapturing( + VideoCaptureOptions(_cameraId, streamCallback: streamCallback)); value = value.copyWith( isRecordingVideo: true, isRecordingPaused: false, + isStreamingImages: onAvailable != null, recordingOrientation: value.lockedCaptureOrientation ?? value.deviceOrientation); } on PlatformException catch (e) { @@ -516,6 +517,11 @@ class CameraController extends ValueNotifier { 'stopVideoRecording was called when no video is recording.', ); } + + if (value.isStreamingImages) { + stopImageStream(); + } + try { final XFile file = await CameraPlatform.instance.stopVideoRecording(_cameraId); diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 33d704ef651f..f8b23bf09db8 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -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.10.1 +version: 0.10.2 environment: sdk: ">=2.14.0 <3.0.0" @@ -21,10 +21,10 @@ flutter: default_package: camera_web dependencies: - camera_android: ^0.10.0 - camera_avfoundation: ^0.9.7+1 - camera_platform_interface: ^2.2.0 - camera_web: ^0.3.0 + camera_android: ^0.10.1 + camera_avfoundation: ^0.9.9 + camera_platform_interface: ^2.3.2 + camera_web: ^0.3.1 flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.2 diff --git a/packages/camera/camera/test/camera_image_stream_test.dart b/packages/camera/camera/test/camera_image_stream_test.dart index a9320e46dfb5..29b5cceaa49a 100644 --- a/packages/camera/camera/test/camera_image_stream_test.dart +++ b/packages/camera/camera/test/camera_image_stream_test.dart @@ -130,7 +130,7 @@ void main() { ); }); - test('stopImageStream() throws $CameraException when recording videos', + test('stopImageStream() throws $CameraException when not streaming images', () async { final CameraController cameraController = CameraController( const CameraDescription( @@ -140,20 +140,16 @@ void main() { ResolutionPreset.max); await cameraController.initialize(); - await cameraController.startImageStream((CameraImage image) => null); - cameraController.value = - cameraController.value.copyWith(isRecordingVideo: true); expect( cameraController.stopImageStream, throwsA(isA().having( (CameraException error) => error.description, - 'A video recording is already started.', - 'stopImageStream was called while a video is being recorded.', + 'No camera is streaming images', + 'stopImageStream was called when no camera is streaming images.', ))); }); - test('stopImageStream() throws $CameraException when not streaming images', - () async { + test('stopImageStream() intended behaviour', () async { final CameraController cameraController = CameraController( const CameraDescription( name: 'cam', @@ -161,29 +157,44 @@ void main() { sensorOrientation: 90), ResolutionPreset.max); await cameraController.initialize(); + await cameraController.startImageStream((CameraImage image) => null); + await cameraController.stopImageStream(); + + expect(mockPlatform.streamCallLog, + ['onStreamedFrameAvailable', 'listen', 'cancel']); + }); + + test('startVideoRecording() can stream images', () async { + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + + cameraController.startVideoRecording( + onAvailable: (CameraImage image) => null); expect( - cameraController.stopImageStream, - throwsA(isA().having( - (CameraException error) => error.description, - 'No camera is streaming images', - 'stopImageStream was called when no camera is streaming images.', - ))); + mockPlatform.streamCallLog.contains('startVideoCapturing with stream'), + isTrue); }); - test('stopImageStream() intended behaviour', () async { + test('startVideoRecording() by default does not stream', () async { final CameraController cameraController = CameraController( const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), ResolutionPreset.max); + await cameraController.initialize(); - await cameraController.startImageStream((CameraImage image) => null); - await cameraController.stopImageStream(); - expect(mockPlatform.streamCallLog, - ['onStreamedFrameAvailable', 'listen', 'cancel']); + cameraController.startVideoRecording(); + + expect(mockPlatform.streamCallLog.contains('startVideoCapturing'), isTrue); }); } @@ -203,6 +214,24 @@ class MockStreamingCameraPlatform extends MockCameraPlatform { return _streamController!.stream; } + @override + Future startVideoRecording(int cameraId, + {Duration? maxVideoDuration}) { + streamCallLog.add('startVideoRecording'); + return super + .startVideoRecording(cameraId, maxVideoDuration: maxVideoDuration); + } + + @override + Future startVideoCapturing(VideoCaptureOptions options) { + if (options.streamCallback == null) { + streamCallLog.add('startVideoCapturing'); + } else { + streamCallLog.add('startVideoCapturing with stream'); + } + return super.startVideoCapturing(options); + } + void _onFrameStreamListen() { streamCallLog.add('listen'); } diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index 546f4e925759..7c4378749ebc 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -97,7 +97,8 @@ class FakeController extends ValueNotifier Future startImageStream(onLatestImageAvailable onAvailable) async {} @override - Future startVideoRecording() async {} + Future startVideoRecording( + {onLatestImageAvailable? onAvailable}) async {} @override Future stopImageStream() async {} diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index 2138f2d055a5..44a48d160d37 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -335,30 +335,6 @@ void main() { ))); }); - test( - 'startVideoRecording() throws $CameraException when already streaming images', - () async { - final CameraController cameraController = CameraController( - const CameraDescription( - name: 'cam', - lensDirection: CameraLensDirection.back, - sensorOrientation: 90), - ResolutionPreset.max); - - await cameraController.initialize(); - - cameraController.value = - cameraController.value.copyWith(isStreamingImages: true); - - expect( - cameraController.startVideoRecording(), - throwsA(isA().having( - (CameraException error) => error.description, - 'A camera has started streaming images.', - 'startVideoRecording was called while a camera was streaming images.', - ))); - }); - test('getMaxZoomLevel() throws $CameraException when uninitialized', () async { final CameraController cameraController = CameraController( @@ -1457,6 +1433,12 @@ class MockCameraPlatform extends Mock {Duration? maxVideoDuration}) => Future.value(mockVideoRecordingXFile); + @override + Future startVideoCapturing(VideoCaptureOptions options) { + return startVideoRecording(options.cameraId, + maxVideoDuration: options.maxDuration); + } + @override Future lockCaptureOrientation( int? cameraId, DeviceOrientation? orientation) async =>