diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index bfb147d20d65..9232ae9a7aea 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.7.4+4 + +* Fix `NullPointerException` when disposing camera during active video recording. + ## 0.7.4+3 * Updates `ResolutionPreset.max` to prefer higher resolution over capture rate 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 41bf38381de5..89601306ddb4 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 @@ -243,4 +243,47 @@ void main() { expect(duration, greaterThanOrEqualTo(const Duration(seconds: 4).inMilliseconds)); await controller.dispose(); }); + + testWidgets('video recording state is cleared after camera is disposed', ( + WidgetTester tester, + ) async { + final List cameras = await availableCameras(); + if (cameras.isEmpty) { + return; + } + + final cameraController = CameraController( + cameras[0], + mediaSettings: const MediaSettings(resolutionPreset: ResolutionPreset.low), + ); + await cameraController.initialize(); + await cameraController.startVideoRecording(); + + // Dispose the controller, which simulates what the example app does + // when the AppLifecycleState becomes inactive (e.g. backgrounding). + await cameraController.dispose(); + + // Create a new controller (simulating app resume) + final newController = CameraController( + cameras[0], + mediaSettings: const MediaSettings(resolutionPreset: ResolutionPreset.low), + ); + await newController.initialize(); + + // Attempt to start a new recording. This should not throw or silently fail. + await newController.startVideoRecording(); + + await Future.delayed(const Duration(seconds: 4)); + + // Stop it, ensuring no NPE is thrown by the native side and the file is valid. + final XFile file = await newController.stopVideoRecording(); + expect(file, isNotNull); + + // Ensure the video was saved correctly + final videoFile = File(file.path); + expect(videoFile.existsSync(), isTrue); + expect(videoFile.lengthSync(), greaterThan(0)); + + await newController.dispose(); + }); } 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 c6d84c4b846f..02698325f0ca 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 @@ -518,6 +518,12 @@ class AndroidCameraCameraX extends CameraPlatform { await processCameraProvider?.unbindAll(); await imageAnalysis?.clearAnalyzer(); await deviceOrientationManager.stopListeningForDeviceOrientationChange(); + + // `processCameraProvider.unbindAll()` implicitly finalizes active recordings natively. + // Clear the Dart state here to prevent an exception on resume. + recording = null; + pendingRecording = null; + videoOutputPath = null; } /// The camera with ID [cameraId] has been initialized. diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index f2bb586173a9..fd1ae9110834 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.7.4+3 +version: 0.7.4+4 environment: sdk: ^3.12.0 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 3130d249ed64..43bb1e15b132 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 @@ -1959,6 +1959,10 @@ void main() { camera.liveCameraState = MockLiveCameraState(); camera.imageAnalysis = MockImageAnalysis(); + camera.recording = MockRecording(); + camera.pendingRecording = MockPendingRecording(); + camera.videoOutputPath = 'test/path.mp4'; + await camera.dispose(3); verify(camera.preview!.releaseSurfaceProvider()); @@ -1966,6 +1970,10 @@ void main() { verify(camera.processCameraProvider!.unbindAll()); verify(camera.imageAnalysis!.clearAnalyzer()); expect(stoppedListeningForDeviceOrientationChange, isTrue); + + expect(camera.recording, isNull); + expect(camera.pendingRecording, isNull); + expect(camera.videoOutputPath, isNull); }, );