diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index fae21094cd7..74d64055d31 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.11.2+1 +* Fixes flutter/flutter#148013: setDescriptionWhileRecording with android camerax. * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. * Updates README to reflect that only Android API 24+ is supported. diff --git a/packages/camera/camera/example/integration_test/camera_test.dart b/packages/camera/camera/example/integration_test/camera_test.dart index 2e4cb68841f..72c102d905f 100644 --- a/packages/camera/camera/example/integration_test/camera_test.dart +++ b/packages/camera/camera/example/integration_test/camera_test.dart @@ -260,7 +260,9 @@ void main() { return completer.future; } - testWidgets('Set description while recording', (WidgetTester tester) async { + testWidgets('Set description while recording captures full video', ( + WidgetTester tester, + ) async { final List cameras = await availableCameras(); if (cameras.length < 2) { return; @@ -269,7 +271,6 @@ void main() { final CameraController controller = CameraController( cameras[0], ResolutionPreset.low, - enableAudio: false, ); await controller.initialize(); @@ -278,7 +279,27 @@ void main() { await controller.startVideoRecording(); await controller.setDescription(cameras[1]); - expect(controller.description, cameras[1]); + await tester.pumpAndSettle(const Duration(seconds: 4)); + + await controller.setDescription(cameras[0]); + + await tester.pumpAndSettle(const Duration(seconds: 1)); + + final XFile file = await controller.stopVideoRecording(); + + final File videoFile = File(file.path); + final VideoPlayerController videoController = VideoPlayerController.file( + videoFile, + ); + await videoController.initialize(); + final int duration = videoController.value.duration.inMilliseconds; + await videoController.dispose(); + + expect( + duration, + greaterThanOrEqualTo(const Duration(seconds: 4).inMilliseconds), + ); + await controller.dispose(); }); testWidgets('Set description', (WidgetTester tester) async { diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml index e268268e320..f6f1f2c098a 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -31,3 +31,8 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + camera_android_camerax: {path: ../../../../packages/camera/camera_android_camerax} + camera_platform_interface: {path: ../../../../packages/camera/camera_platform_interface} diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 0cd0fb0878f..433d6c20972 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -439,6 +439,10 @@ class CameraController extends ValueNotifier { /// Sets the description of the camera. /// + /// On Android, you must start the recording with [startVideoRecording] + /// with `enablePersistentRecording` set to `true` + /// to avoid cancelling any active recording. + /// /// Throws a [CameraException] if setting the description fails. Future setDescription(CameraDescription description) async { if (value.isRecordingVideo) { @@ -554,8 +558,15 @@ class CameraController extends ValueNotifier { /// /// The video is returned as a [XFile] after calling [stopVideoRecording]. /// Throws a [CameraException] if the capture fails. + /// + /// `enablePersistentRecording` parameter configures the recording to be a persistent recording. + /// A persistent recording can only be stopped by explicitly calling [stopVideoRecording] + /// and will ignore events that would normally cause recording to stop, + /// such as lifecycle events or explicit calls to [setDescription] while recording is in progress. + /// Currently a no-op on platforms other than Android. Future startVideoRecording({ onLatestImageAvailable? onAvailable, + bool enablePersistentRecording = true, }) async { _throwIfNotInitialized('startVideoRecording'); if (value.isRecordingVideo) { @@ -574,7 +585,11 @@ class CameraController extends ValueNotifier { try { await CameraPlatform.instance.startVideoCapturing( - VideoCaptureOptions(_cameraId, streamCallback: streamCallback), + VideoCaptureOptions( + _cameraId, + streamCallback: streamCallback, + enablePersistentRecording: enablePersistentRecording, + ), ); value = value.copyWith( isRecordingVideo: true, diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 073c8019872..5ab069a44ca 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/packages/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.11.2 +version: 0.11.2+1 environment: sdk: ^3.7.0 @@ -38,3 +38,8 @@ dev_dependencies: topics: - camera +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + camera_android_camerax: {path: ../../../packages/camera/camera_android_camerax} + camera_platform_interface: {path: ../../../packages/camera/camera_platform_interface} diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index ff296bec5ad..c84223b92ec 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -111,6 +111,7 @@ class FakeController extends ValueNotifier @override Future startVideoRecording({ onLatestImageAvailable? onAvailable, + bool enablePersistentRecording = true, }) async {} @override diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 40159b68c97..e85215ca9ad 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.22 + +* Implements `setDescriptionWhileRecording`. + ## 0.6.21 * Implements NV21 support for image streaming. diff --git a/packages/camera/camera_android_camerax/README.md b/packages/camera/camera_android_camerax/README.md index b6a61aa1cb7..5cf163e19a3 100644 --- a/packages/camera/camera_android_camerax/README.md +++ b/packages/camera/camera_android_camerax/README.md @@ -34,10 +34,6 @@ use cases, the plugin behaves according to the following: video recording and image streaming is supported, but concurrent video recording, image streaming, and image capture is not supported. -### `setDescriptionWhileRecording` is unimplemented [Issue #148013][148013] -`setDescriptionWhileRecording`, used to switch cameras while recording video, is currently unimplemented -due to this not currently being supported by CameraX. - ### 240p resolution configuration for video recording 240p resolution configuration for video recording is unsupported by CameraX, and thus, @@ -73,6 +69,12 @@ in the merged Android manifest of your app, then take the following steps to rem tools:node="remove" /> ``` +### Notes on video capture + +#### Setting description while recording +To avoid cancelling any active recording when calling `setDescriptionWhileRecording`, +you must start the recording with `startVideoCapturing` with `enablePersistentRecording` set to `true`. + ### Notes on image streaming #### Allowing image streaming in the background diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt index f4a84496acc..af88f641a65 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt @@ -3652,6 +3652,22 @@ abstract class PigeonApiPendingRecording( initialMuted: Boolean ): androidx.camera.video.PendingRecording + /** + * Configures the recording to be a persistent recording. + * + * A persistent recording will only be stopped by explicitly calling [Recording.stop] or + * [Recording.close] and will ignore events that would normally cause recording to stop, such as + * lifecycle events or explicit unbinding of a [VideoCapture] use case that the recording's + * Recorder is attached to. + * + * To switch to a different camera stream while a recording is in progress, first create the + * recording as persistent recording, then rebind the [VideoCapture] it's associated with to a + * different camera. + */ + abstract fun asPersistentRecording( + pigeon_instance: androidx.camera.video.PendingRecording + ): androidx.camera.video.PendingRecording + /** Starts the recording, making it an active recording. */ abstract fun start( pigeon_instance: androidx.camera.video.PendingRecording, @@ -3685,6 +3701,28 @@ abstract class PigeonApiPendingRecording( channel.setMessageHandler(null) } } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.camera_android_camerax.PendingRecording.asPersistentRecording", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val pigeon_instanceArg = args[0] as androidx.camera.video.PendingRecording + val wrapped: List = + try { + listOf(api.asPersistentRecording(pigeon_instanceArg)) + } catch (exception: Throwable) { + CameraXLibraryPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel( diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingProxyApi.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingProxyApi.java index 14fef1b0ef9..f6e11f46b34 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingProxyApi.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingProxyApi.java @@ -7,6 +7,7 @@ import android.Manifest; import android.content.pm.PackageManager; import androidx.annotation.NonNull; +import androidx.camera.video.ExperimentalPersistentRecording; import androidx.camera.video.PendingRecording; import androidx.camera.video.Recording; import androidx.core.content.ContextCompat; @@ -27,6 +28,13 @@ public ProxyApiRegistrar getPigeonRegistrar() { return (ProxyApiRegistrar) super.getPigeonRegistrar(); } + @ExperimentalPersistentRecording + @NonNull + @Override + public PendingRecording asPersistentRecording(PendingRecording pigeonInstance) { + return pigeonInstance.asPersistentRecording(); + } + @NonNull @Override public PendingRecording withAudioEnabled(PendingRecording pigeonInstance, boolean initialMuted) { diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PendingRecordingTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PendingRecordingTest.java index 0883191b4dc..5bdf847627b 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PendingRecordingTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PendingRecordingTest.java @@ -83,6 +83,19 @@ public void withAudioEnabled_doesNotEnableAudioWhenAudioNotRequested() { verify(instance).withAudioEnabled(true); } + @Test + public void asPersistentRecording_returnsPersistentRecordingInstance() { + final PigeonApiPendingRecording api = + new TestProxyApiRegistrar().getPigeonApiPendingRecording(); + final PendingRecording instance = mock(PendingRecording.class); + final PendingRecording persistentInstance = mock(PendingRecording.class); + + when(instance.asPersistentRecording()).thenReturn(persistentInstance); + + assertEquals(persistentInstance, api.asPersistentRecording(instance)); + verify(instance).asPersistentRecording(); + } + @Test public void start_callsStartOnInstance() { final PigeonApiPendingRecording api = diff --git a/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart b/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart index af11f5be2cb..92d4fa0f8e5 100644 --- a/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart +++ b/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart @@ -226,4 +226,49 @@ void main() { expect(duration, lessThan(recordingTime - timePaused)); }, skip: skipFor157181); + + testWidgets('Set description while recording captures full video', ( + WidgetTester tester, + ) async { + final List cameras = await availableCameras(); + if (cameras.length < 2) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + mediaSettings: const MediaSettings( + resolutionPreset: ResolutionPreset.medium, + enableAudio: true, + ), + ); + await controller.initialize(); + await controller.prepareForVideoRecording(); + + await controller.startVideoRecording(); + + await controller.setDescription(cameras[1]); + + await tester.pumpAndSettle(const Duration(seconds: 4)); + + await controller.setDescription(cameras[0]); + + await tester.pumpAndSettle(const Duration(seconds: 1)); + + final XFile file = await controller.stopVideoRecording(); + + final File videoFile = File(file.path); + final VideoPlayerController videoController = VideoPlayerController.file( + videoFile, + ); + await videoController.initialize(); + final int duration = videoController.value.duration.inMilliseconds; + await videoController.dispose(); + + expect( + duration, + greaterThanOrEqualTo(const Duration(seconds: 4).inMilliseconds), + ); + await controller.dispose(); + }); } diff --git a/packages/camera/camera_android_camerax/example/lib/camera_controller.dart b/packages/camera/camera_android_camerax/example/lib/camera_controller.dart index 49e2821f8dd..17f1b46487e 100644 --- a/packages/camera/camera_android_camerax/example/lib/camera_controller.dart +++ b/packages/camera/camera_android_camerax/example/lib/camera_controller.dart @@ -49,6 +49,7 @@ class CameraValue { required this.exposurePointSupported, required this.focusPointSupported, required this.deviceOrientation, + required this.description, this.lockedCaptureOrientation, this.recordingOrientation, this.isPreviewPaused = false, @@ -56,7 +57,7 @@ class CameraValue { }) : _isRecordingPaused = isRecordingPaused; /// Creates a new camera controller state for an uninitialized controller. - const CameraValue.uninitialized() + const CameraValue.uninitialized(CameraDescription description) : this( isInitialized: false, isRecordingVideo: false, @@ -70,6 +71,7 @@ class CameraValue { focusPointSupported: false, deviceOrientation: DeviceOrientation.portraitUp, isPreviewPaused: false, + description: description, ); /// True after [CameraController.initialize] has completed successfully. @@ -143,6 +145,9 @@ class CameraValue { /// The orientation of the currently running video recording. final DeviceOrientation? recordingOrientation; + /// The properties of the camera device controlled by this controller. + final CameraDescription description; + /// Creates a modified copy of the object. /// /// Explicitly specified fields get the specified value, all other fields get @@ -164,6 +169,7 @@ class CameraValue { Optional? lockedCaptureOrientation, Optional? recordingOrientation, bool? isPreviewPaused, + CameraDescription? description, Optional? previewPauseOrientation, }) { return CameraValue( @@ -190,6 +196,7 @@ class CameraValue { ? this.recordingOrientation : recordingOrientation.orNull, isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, + description: description ?? this.description, previewPauseOrientation: previewPauseOrientation == null ? this.previewPauseOrientation @@ -214,7 +221,8 @@ class CameraValue { 'lockedCaptureOrientation: $lockedCaptureOrientation, ' 'recordingOrientation: $recordingOrientation, ' 'isPreviewPaused: $isPreviewPaused, ' - 'previewPausedOrientation: $previewPauseOrientation)'; + 'previewPausedOrientation: $previewPauseOrientation, ' + 'description: $description)'; } } @@ -228,13 +236,13 @@ class CameraValue { class CameraController extends ValueNotifier { /// Creates a new camera controller in an uninitialized state. CameraController( - this.description, { + CameraDescription description, { this.mediaSettings, this.imageFormatGroup, - }) : super(const CameraValue.uninitialized()); + }) : super(CameraValue.uninitialized(description)); /// The properties of the camera device controlled by this controller. - final CameraDescription description; + CameraDescription get description => value.description; /// The media settings this controller is targeting. /// @@ -273,7 +281,12 @@ class CameraController extends ValueNotifier { /// Initializes the camera on the device. /// /// Throws a [CameraException] if the initialization fails. - Future initialize() async { + Future initialize() => _initializeWithDescription(description); + + /// Initializes the camera on the device with the specified description. + /// + /// Throws a [CameraException] if the initialization fails. + Future _initializeWithDescription(CameraDescription description) async { if (_isDisposed) { throw CameraException( 'Disposed CameraController', @@ -489,8 +502,14 @@ class CameraController extends ValueNotifier { /// /// The video is returned as a [XFile] after calling [stopVideoRecording]. /// Throws a [CameraException] if the capture fails. + /// + /// `enablePersistentRecording` parameter configures the recording to be a persistent recording. + /// A persistent recording will only be stopped by explicitly calling [stopVideoRecording] + /// and will ignore events that would normally cause recording to stop, + /// such as lifecycle events or explicit calls to [setDescription] while recording is in progress. Future startVideoRecording({ onLatestImageAvailable? onAvailable, + bool enablePersistentRecording = true, }) async { _throwIfNotInitialized('startVideoRecording'); if (value.isRecordingVideo) { @@ -509,7 +528,11 @@ class CameraController extends ValueNotifier { try { await CameraPlatform.instance.startVideoCapturing( - VideoCaptureOptions(_cameraId, streamCallback: streamCallback), + VideoCaptureOptions( + _cameraId, + streamCallback: streamCallback, + enablePersistentRecording: enablePersistentRecording, + ), ); value = value.copyWith( isRecordingVideo: true, @@ -592,6 +615,21 @@ class CameraController extends ValueNotifier { } } + /// Sets the description of the camera. + /// + /// To avoid cancelling any active recording when calling this method, + /// start the recording with [startVideoRecording] with `enablePersistentRecording` to `true`. + /// + /// Throws a [CameraException] if setting the description fails. + Future setDescription(CameraDescription description) async { + if (value.isRecordingVideo) { + await CameraPlatform.instance.setDescriptionWhileRecording(description); + value = value.copyWith(description: description); + } else { + await _initializeWithDescription(description); + } + } + /// Returns a widget showing a live camera preview. Widget buildPreview() { _throwIfNotInitialized('buildPreview'); diff --git a/packages/camera/camera_android_camerax/example/lib/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart index 359f584fd76..16e5a7dfdf0 100644 --- a/packages/camera/camera_android_camerax/example/lib/main.dart +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -123,7 +123,7 @@ class _CameraExampleHomeState extends State if (state == AppLifecycleState.inactive) { cameraController.dispose(); } else if (state == AppLifecycleState.resumed) { - onNewCameraSelected(cameraController.description); + _initializeCameraController(cameraController.description); } } // #enddocregion AppLifecycle @@ -611,10 +611,7 @@ class _CameraExampleHomeState extends State title: Icon(getCameraLensIcon(cameraDescription.lensDirection)), groupValue: controller?.description, value: cameraDescription, - onChanged: - controller != null && controller!.value.isRecordingVideo - ? null - : onChanged, + onChanged: onChanged, ), ), ); @@ -648,17 +645,16 @@ class _CameraExampleHomeState extends State } Future onNewCameraSelected(CameraDescription cameraDescription) async { - final CameraController? oldController = controller; - if (oldController != null) { - // `controller` needs to be set to null before getting disposed, - // to avoid a race condition when we use the controller that is being - // disposed. This happens when camera permission dialog shows up, - // which triggers `didChangeAppLifecycleState`, which disposes and - // re-creates the controller. - controller = null; - await oldController.dispose(); + if (controller != null) { + return controller!.setDescription(cameraDescription); + } else { + return _initializeCameraController(cameraDescription); } + } + Future _initializeCameraController( + CameraDescription cameraDescription, + ) async { final CameraController cameraController = CameraController( cameraDescription, mediaSettings: MediaSettings( diff --git a/packages/camera/camera_android_camerax/example/pubspec.yaml b/packages/camera/camera_android_camerax/example/pubspec.yaml index f5238212492..73953695de9 100644 --- a/packages/camera/camera_android_camerax/example/pubspec.yaml +++ b/packages/camera/camera_android_camerax/example/pubspec.yaml @@ -28,4 +28,8 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + camera_platform_interface: {path: ../../../../packages/camera/camera_platform_interface} diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart index 5527ce65d64..2644a9d56a6 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -927,11 +927,46 @@ class AndroidCameraCameraX extends CameraPlatform { /// Sets the active camera while recording. /// - /// Currently unsupported, so is a no-op. + /// To avoid cancelling any active recording when this method is called, + /// you must start the recording with [startVideoCapturing] + /// with `enablePersistentRecording` set to `true`. @override - Future setDescriptionWhileRecording(CameraDescription description) { - // TODO(camsim99): Implement this feature, see https://github.com/flutter/flutter/issues/148013. - return Future.value(); + Future setDescriptionWhileRecording( + CameraDescription description, + ) async { + if (recording == null) { + cameraErrorStreamController.add( + 'Camera description not set. No active video recording.', + ); + return; + } + final CameraInfo? chosenCameraInfo = _savedCameras[description.name]; + + // Save CameraSelector that matches cameraDescription. + final LensFacing cameraSelectorLensDirection = + _getCameraSelectorLensDirection(description.lensDirection); + cameraIsFrontFacing = cameraSelectorLensDirection == LensFacing.front; + cameraSelector = proxy.newCameraSelector( + cameraInfoForFilter: chosenCameraInfo, + ); + + // Unbind all use cases and rebind to new CameraSelector + final List useCases = [preview!, videoCapture!]; + if (imageCapture != null && + await processCameraProvider!.isBound(imageCapture!)) { + useCases.add(imageCapture!); + } + if (imageAnalysis != null && + await processCameraProvider!.isBound(imageAnalysis!)) { + useCases.add(imageAnalysis!); + } + await processCameraProvider?.unbindAll(); + await processCameraProvider?.bindToLifecycle(cameraSelector!, useCases); + + // Retrieve info required for correcting the rotation of the camera preview + sensorOrientationDegrees = description.sensorOrientation.toDouble(); + + await _updateCameraInfoAndLiveCameraState(_flutterSurfaceTextureId); } /// Resume the paused preview for the selected camera. @@ -1140,6 +1175,10 @@ class AndroidCameraCameraX extends CameraPlatform { ); pendingRecording = await recorder!.prepareRecording(videoOutputPath!); + if (options.enablePersistentRecording) { + pendingRecording = await pendingRecording?.asPersistentRecording(); + } + // Enable/disable recording audio as requested. If enabling audio is requested // and permission was not granted when the camera was created, then recording // audio will be disabled to respect the denied permission. diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart index b1e1f737ac3..2b07654042d 100644 --- a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart +++ b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart @@ -4404,6 +4404,50 @@ class PendingRecording extends PigeonInternalProxyApiBaseClass { } } + /// Configures the recording to be a persistent recording. + /// + /// A persistent recording will only be stopped by explicitly calling [Recording.stop] or [Recording.close] + /// and will ignore events that would normally cause recording to stop, such as lifecycle events + /// or explicit unbinding of a [VideoCapture] use case that the recording's Recorder is attached to. + /// + /// To switch to a different camera stream while a recording is in progress, + /// first create the recording as persistent recording, + /// then rebind the [VideoCapture] it's associated with to a different camera. + Future asPersistentRecording() async { + final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec = + _pigeonVar_codecPendingRecording; + final BinaryMessenger? pigeonVar_binaryMessenger = pigeon_binaryMessenger; + const String pigeonVar_channelName = + 'dev.flutter.pigeon.camera_android_camerax.PendingRecording.asPersistentRecording'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [this], + ); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as PendingRecording?)!; + } + } + /// Starts the recording, making it an active recording. Future start(VideoRecordEventListener listener) async { final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec = diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index f9114df3f7c..dfe492b8dbc 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -523,6 +523,17 @@ abstract class PendingRecording { /// Enables/disables audio to be recorded for this recording. PendingRecording withAudioEnabled(bool initialMuted); + /// Configures the recording to be a persistent recording. + /// + /// A persistent recording will only be stopped by explicitly calling [Recording.stop] or [Recording.close] + /// and will ignore events that would normally cause recording to stop, such as lifecycle events + /// or explicit unbinding of a [VideoCapture] use case that the recording's Recorder is attached to. + /// + /// To switch to a different camera stream while a recording is in progress, + /// first create the recording as persistent recording, + /// then rebind the [VideoCapture] it's associated with to a different camera. + PendingRecording asPersistentRecording(); + /// Starts the recording, making it an active recording. Recording start(VideoRecordEventListener listener); } diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index eeed6e6a4c1..d0fc3e027d5 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.6.21 +version: 0.6.22 environment: sdk: ^3.8.1 @@ -35,3 +35,7 @@ dev_dependencies: topics: - camera +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + camera_platform_interface: {path: ../../../packages/camera/camera_platform_interface} diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart index 766c4ddedfe..59a86c7dd75 100644 --- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart @@ -3346,6 +3346,9 @@ void main() { when( mockPendingRecording.withAudioEnabled(!enableAudio), ).thenAnswer((_) async => mockPendingRecordingWithAudio); + when( + mockPendingRecording.asPersistentRecording(), + ).thenAnswer((_) async => mockPendingRecording); when( mockPendingRecordingWithAudio.start(any), ).thenAnswer((_) async => mockRecording); @@ -3512,6 +3515,9 @@ void main() { when( mockPendingRecording.withAudioEnabled(!camera.enableRecordingAudio), ).thenAnswer((_) async => mockPendingRecording); + when( + mockPendingRecording.asPersistentRecording(), + ).thenAnswer((_) async => mockPendingRecording); when( mockPendingRecording.start(any), ).thenAnswer((_) async => mockRecording); @@ -3567,6 +3573,7 @@ void main() { verifyNoMoreInteractions(camera.recorder); verify(mockPendingRecording.start(any)).called(1); verify(mockPendingRecording.withAudioEnabled(any)).called(1); + verify(mockPendingRecording.asPersistentRecording()).called(1); verifyNoMoreInteractions(mockPendingRecording); }, ); @@ -3701,6 +3708,9 @@ void main() { when( mockPendingRecording.withAudioEnabled(!camera.enableRecordingAudio), ).thenAnswer((_) async => mockPendingRecording); + when( + mockPendingRecording.asPersistentRecording(), + ).thenAnswer((_) async => mockPendingRecording); when( mockProcessCameraProvider.bindToLifecycle(any, any), ).thenAnswer((_) => Future.value(camera.camera)); @@ -3850,6 +3860,9 @@ void main() { when( mockPendingRecording.withAudioEnabled(!camera.enableRecordingAudio), ).thenAnswer((_) async => mockPendingRecording); + when( + mockPendingRecording.asPersistentRecording(), + ).thenAnswer((_) async => mockPendingRecording); when( mockPendingRecording.start(any), ).thenAnswer((_) async => mockRecording); @@ -4106,31 +4119,318 @@ void main() { }, ); - test( - 'setDescriptionWhileRecording does not make any calls involving starting video recording', - () async { - // TODO(camsim99): Modify test when implemented, see https://github.com/flutter/flutter/issues/148013. - final AndroidCameraCameraX camera = AndroidCameraCameraX(); + test('setDescriptionWhileRecording changes the camera description', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockRecording mockRecording = MockRecording(); + final MockPendingRecording mockPendingRecording = MockPendingRecording(); + final MockRecorder mockRecorder = MockRecorder(); - // Set directly for test versus calling createCamera. - camera.processCameraProvider = MockProcessCameraProvider(); - camera.recorder = MockRecorder(); - camera.videoCapture = MockVideoCapture(); - camera.camera = MockCamera(); + const int testSensorOrientation = 90; + const CameraDescription testBackCameraDescription = CameraDescription( + name: 'Camera 0', + lensDirection: CameraLensDirection.back, + sensorOrientation: testSensorOrientation, + ); + const CameraDescription testFrontCameraDescription = CameraDescription( + name: 'Camera 1', + lensDirection: CameraLensDirection.front, + sensorOrientation: testSensorOrientation, + ); + + // Mock/Detached objects for (typically attached) objects created by + // createCamera. + final MockProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); + final MockPreview mockPreview = MockPreview(); + final MockCamera mockCamera = MockCamera(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final MockCameraControl mockCameraControl = MockCameraControl(); + final MockImageCapture mockImageCapture = MockImageCapture(); + final MockImageAnalysis mockImageAnalysis = MockImageAnalysis(); + final MockVideoCapture mockVideoCapture = MockVideoCapture(); + final MockCameraSelector mockBackCameraSelector = MockCameraSelector(); + final MockCameraSelector mockFrontCameraSelector = MockCameraSelector(); + final MockCameraInfo mockFrontCameraInfo = MockCameraInfo(); + final MockCameraInfo mockBackCameraInfo = MockCameraInfo(); + final MockCameraCharacteristicsKey mockCameraCharacteristicsKey = + MockCameraCharacteristicsKey(); + + const String outputPath = 'file/output.mp4'; + + camera.proxy = CameraXProxy( + newPreview: + ({ + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + ResolutionSelector? resolutionSelector, + int? targetRotation, + }) { + when( + mockPreview.setSurfaceProvider(any), + ).thenAnswer((_) async => 19); + final ResolutionInfo testResolutionInfo = + ResolutionInfo.pigeon_detached(resolution: MockCameraSize()); + when( + mockPreview.surfaceProducerHandlesCropAndRotation(), + ).thenAnswer((_) async => false); + when( + mockPreview.resolutionSelector, + ).thenReturn(resolutionSelector); + when( + mockPreview.getResolutionInfo(), + ).thenAnswer((_) async => testResolutionInfo); + return mockPreview; + }, + newImageCapture: + ({ + CameraXFlashMode? flashMode, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + ResolutionSelector? resolutionSelector, + int? targetRotation, + }) { + return mockImageCapture; + }, + newRecorder: + ({ + int? aspectRatio, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + QualitySelector? qualitySelector, + int? targetVideoEncodingBitRate, + }) { + when( + mockRecorder.prepareRecording(outputPath), + ).thenAnswer((_) async => mockPendingRecording); + return mockRecorder; + }, + withOutputVideoCapture: + ({ + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + required VideoOutput videoOutput, + }) { + return mockVideoCapture; + }, + newImageAnalysis: + ({ + int? outputImageFormat, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + ResolutionSelector? resolutionSelector, + int? targetRotation, + }) { + return mockImageAnalysis; + }, + newCameraSelector: + ({ + LensFacing? requireLensFacing, + CameraInfo? cameraInfoForFilter, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + if (cameraInfoForFilter == mockFrontCameraInfo) { + return mockFrontCameraSelector; + } + return mockBackCameraSelector; + }, + newDeviceOrientationManager: + ({ + required void Function(DeviceOrientationManager, String) + onDeviceOrientationChanged, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + final MockDeviceOrientationManager manager = + MockDeviceOrientationManager(); + when(manager.getUiOrientation()).thenAnswer((_) async { + return 'PORTRAIT_UP'; + }); + return manager; + }, + fromCamera2CameraInfo: + ({ + required CameraInfo cameraInfo, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + final MockCamera2CameraInfo camera2cameraInfo = + MockCamera2CameraInfo(); + when( + camera2cameraInfo.getCameraCharacteristic(any), + ).thenAnswer((_) async => InfoSupportedHardwareLevel.limited); + return camera2cameraInfo; + }, + sensorOrientationCameraCharacteristics: () { + return mockCameraCharacteristicsKey; + }, + newObserver: + ({ + required void Function(Observer, T) onChanged, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + return Observer.detached( + onChanged: onChanged, + pigeon_instanceManager: PigeonInstanceManager( + onWeakReferenceRemoved: (_) {}, + ), + ); + }, + newSystemServicesManager: + ({ + required void Function(SystemServicesManager, String) + onCameraError, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + final MockSystemServicesManager mockSystemServicesManager = + MockSystemServicesManager(); + when( + mockSystemServicesManager.getTempFilePath( + camera.videoPrefix, + '.temp', + ), + ).thenAnswer((_) async => outputPath); + return mockSystemServicesManager; + }, + newVideoRecordEventListener: + ({ + required void Function(VideoRecordEventListener, VideoRecordEvent) + onEvent, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + return VideoRecordEventListener.pigeon_detached( + onEvent: onEvent, + pigeon_instanceManager: PigeonInstanceManager( + onWeakReferenceRemoved: (_) {}, + ), + ); + }, + infoSupportedHardwareLevelCameraCharacteristics: () { + return MockCameraCharacteristicsKey(); + }, + ); - await camera.setDescriptionWhileRecording( - const CameraDescription( - name: 'fakeCameraName', - lensDirection: CameraLensDirection.back, - sensorOrientation: 90, + // mock functions + when(mockProcessCameraProvider.getAvailableCameraInfos()).thenAnswer( + (_) async => [mockBackCameraInfo, mockFrontCameraInfo], + ); + when( + mockProcessCameraProvider.bindToLifecycle(any, any), + ).thenAnswer((_) async => mockCamera); + when( + mockBackCameraSelector.filter([mockBackCameraInfo]), + ).thenAnswer((_) async => [mockBackCameraInfo]); + when( + mockBackCameraSelector.filter([mockFrontCameraInfo]), + ).thenAnswer((_) async => [mockFrontCameraInfo]); + when( + mockFrontCameraSelector.filter([mockBackCameraInfo]), + ).thenAnswer((_) async => [mockBackCameraInfo]); + when( + mockFrontCameraSelector.filter([mockFrontCameraInfo]), + ).thenAnswer((_) async => [mockFrontCameraInfo]); + + camera.processCameraProvider = mockProcessCameraProvider; + camera.enableRecordingAudio = false; + when( + mockPendingRecording.withAudioEnabled(any), + ).thenAnswer((_) async => mockPendingRecording); + when( + mockPendingRecording.asPersistentRecording(), + ).thenAnswer((_) async => mockPendingRecording); + when( + mockPendingRecording.start(any), + ).thenAnswer((_) async => mockRecording); + when( + camera.processCameraProvider!.isBound(mockImageCapture), + ).thenAnswer((_) async => true); + when( + camera.processCameraProvider!.isBound(mockImageAnalysis), + ).thenAnswer((_) async => true); + when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); + when( + mockCameraInfo.getCameraState(), + ).thenAnswer((_) async => MockLiveCameraState()); + when( + mockCameraInfo.getCameraState(), + ).thenAnswer((_) async => MockLiveCameraState()); + when(mockCamera.cameraControl).thenAnswer((_) => mockCameraControl); + + // Simulate video recording being started so startVideoRecording completes. + AndroidCameraCameraX.videoRecordingEventStreamController.add( + VideoRecordEventStart.pigeon_detached( + pigeon_instanceManager: PigeonInstanceManager( + onWeakReferenceRemoved: (_) {}, ), - ); - verifyNoMoreInteractions(camera.processCameraProvider); - verifyNoMoreInteractions(camera.recorder); - verifyNoMoreInteractions(camera.videoCapture); - verifyNoMoreInteractions(camera.camera); - }, - ); + ), + ); + + await camera.availableCameras(); + + final int flutterSurfaceTextureId = await camera.createCameraWithSettings( + testBackCameraDescription, + const MediaSettings(enableAudio: true), + ); + await camera.initializeCamera(flutterSurfaceTextureId); + + await camera.startVideoCapturing( + VideoCaptureOptions(flutterSurfaceTextureId), + ); + await camera.setDescriptionWhileRecording(testFrontCameraDescription); + + //verify front camera selected + verify(camera.processCameraProvider?.unbindAll()).called(2); + verify( + camera.processCameraProvider?.bindToLifecycle( + mockFrontCameraSelector, + [ + mockPreview, + mockVideoCapture, + mockImageCapture, + mockImageAnalysis, + ], + ), + ).called(1); + + //verify back camera selected + await camera.setDescriptionWhileRecording(testBackCameraDescription); + verify( + camera.processCameraProvider?.bindToLifecycle( + mockBackCameraSelector, + [ + mockPreview, + mockVideoCapture, + mockImageCapture, + mockImageAnalysis, + ], + ), + ).called(1); + }); }); test( @@ -6891,6 +7191,9 @@ void main() { when( mockPendingRecording.withAudioEnabled(!camera.enableRecordingAudio), ).thenAnswer((_) async => mockPendingRecording); + when( + mockPendingRecording.asPersistentRecording(), + ).thenAnswer((_) async => mockPendingRecording); when( mockPendingRecording.start(any), ).thenAnswer((_) async => mockRecording); @@ -7032,6 +7335,9 @@ void main() { when( mockPendingRecording.withAudioEnabled(!camera.enableRecordingAudio), ).thenAnswer((_) async => mockPendingRecording); + when( + mockPendingRecording.asPersistentRecording(), + ).thenAnswer((_) async => mockPendingRecording); when( mockPendingRecording.start(any), ).thenAnswer((_) async => mockRecording); @@ -7173,6 +7479,9 @@ void main() { when( mockPendingRecording.withAudioEnabled(!camera.enableRecordingAudio), ).thenAnswer((_) async => mockPendingRecording); + when( + mockPendingRecording.asPersistentRecording(), + ).thenAnswer((_) async => mockPendingRecording); when( mockPendingRecording.start(any), ).thenAnswer((_) async => mockRecording); @@ -7334,6 +7643,9 @@ void main() { when( mockPendingRecording.withAudioEnabled(!camera.enableRecordingAudio), ).thenAnswer((_) async => mockPendingRecording); + when( + mockPendingRecording.asPersistentRecording(), + ).thenAnswer((_) async => mockPendingRecording); when( mockPendingRecording.start(any), ).thenAnswer((_) async => mockRecording); @@ -7499,6 +7811,9 @@ void main() { when( mockPendingRecording.withAudioEnabled(!camera.enableRecordingAudio), ).thenAnswer((_) async => mockPendingRecording); + when( + mockPendingRecording.asPersistentRecording(), + ).thenAnswer((_) async => mockPendingRecording); when( mockPendingRecording.start(any), ).thenAnswer((_) async => mockRecording); @@ -7656,6 +7971,9 @@ void main() { when( mockPendingRecording.withAudioEnabled(!camera.enableRecordingAudio), ).thenAnswer((_) async => mockPendingRecording); + when( + mockPendingRecording.asPersistentRecording(), + ).thenAnswer((_) async => mockPendingRecording); when( mockPendingRecording.start(any), ).thenAnswer((_) async => mockRecording); diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart index de4575d8041..1af9e9207f7 100644 --- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart @@ -3029,6 +3029,25 @@ class MockPendingRecording extends _i1.Mock implements _i2.PendingRecording { ) as _i5.Future<_i2.PendingRecording>); + @override + _i5.Future<_i2.PendingRecording> asPersistentRecording() => + (super.noSuchMethod( + Invocation.method(#asPersistentRecording, []), + returnValue: _i5.Future<_i2.PendingRecording>.value( + _FakePendingRecording_39( + this, + Invocation.method(#asPersistentRecording, []), + ), + ), + returnValueForMissingStub: _i5.Future<_i2.PendingRecording>.value( + _FakePendingRecording_39( + this, + Invocation.method(#asPersistentRecording, []), + ), + ), + ) + as _i5.Future<_i2.PendingRecording>); + @override _i5.Future<_i2.Recording> start(_i2.VideoRecordEventListener? listener) => (super.noSuchMethod( diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index 9e26946eca0..e2e21cb10f3 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.11.0 +* Fixes flutter/flutter#148013: setDescriptionWhileRecording with android camerax. * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. ## 2.10.0 diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart index f5959a74689..8d3818f6db0 100644 --- a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -292,6 +292,10 @@ abstract class CameraPlatform extends PlatformInterface { } /// Sets the active camera while recording. + /// + /// On Android, you must start the recording with [startVideoCapturing] + /// with `enablePersistentRecording` set to `true` + /// to avoid cancelling any active recording. Future setDescriptionWhileRecording(CameraDescription description) { throw UnimplementedError( 'setDescriptionWhileRecording() is not implemented.', diff --git a/packages/camera/camera_platform_interface/lib/src/types/video_capture_options.dart b/packages/camera/camera_platform_interface/lib/src/types/video_capture_options.dart index e5b32103e79..bba1b90224f 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/video_capture_options.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/video_capture_options.dart @@ -18,6 +18,7 @@ class VideoCaptureOptions { this.maxDuration, this.streamCallback, this.streamOptions, + this.enablePersistentRecording = true, }) : assert( streamOptions == null || streamCallback != null, 'Must specify streamCallback if providing streamOptions.', @@ -43,6 +44,17 @@ class VideoCaptureOptions { /// Should only be set if a streamCallback is also present. final CameraImageStreamOptions? streamOptions; + /// Configures the recording to be a persistent recording. + /// + /// A persistent recording can only be stopped by explicitly calling [CameraController.stopVideoRecording] + /// and will ignore events that would normally cause recording to stop, such as lifecycle events. + /// + /// On Android, you must set this parameter to `true` + /// to avoid cancelling any active recording when calling [CameraController.setDescription]. + /// + /// Defaults to `true`. + final bool enablePersistentRecording; + @override bool operator ==(Object other) => identical(this, other) || @@ -51,9 +63,15 @@ class VideoCaptureOptions { cameraId == other.cameraId && maxDuration == other.maxDuration && streamCallback == other.streamCallback && - streamOptions == other.streamOptions; + streamOptions == other.streamOptions && + enablePersistentRecording == other.enablePersistentRecording; @override - int get hashCode => - Object.hash(cameraId, maxDuration, streamCallback, streamOptions); + int get hashCode => Object.hash( + cameraId, + maxDuration, + streamCallback, + streamOptions, + enablePersistentRecording, + ); } diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index 8ea2589400b..ec310739676 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/packages/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.10.0 +version: 2.11.0 environment: sdk: ^3.7.0