From 21e3b8a24e91ea62a10349f61e1f513a0ce72b3d Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Mon, 20 Mar 2023 10:55:23 -0600 Subject: [PATCH 01/14] Recreating PR from flutter/plugins --- packages/camera/camera/CHANGELOG.md | 4 + packages/camera/camera/README.md | 2 +- .../example/integration_test/camera_test.dart | 43 +++ packages/camera/camera/example/lib/main.dart | 23 +- .../camera/lib/src/camera_controller.dart | 38 +- packages/camera/camera/pubspec.yaml | 12 +- .../camera/test/camera_preview_test.dart | 15 +- .../camera/camera/test/camera_value_test.dart | 85 ++-- packages/camera/camera_android/CHANGELOG.md | 4 + .../io/flutter/plugins/camera/Camera.java | 167 ++++++-- .../plugins/camera/MethodCallHandlerImpl.java | 12 + .../flutter/plugins/camera/VideoRenderer.java | 365 ++++++++++++++++++ .../io/flutter/plugins/camera/CameraTest.java | 109 ++++++ .../example/integration_test/camera_test.dart | 45 +++ .../example/lib/camera_controller.dart | 30 +- .../camera_android/example/lib/main.dart | 23 +- .../camera_android/example/pubspec.yaml | 8 +- .../lib/src/android_camera.dart | 11 + packages/camera/camera_android/pubspec.yaml | 2 +- .../test/android_camera_test.dart | 23 ++ .../camera_android_camerax/pubspec.yaml | 1 + .../camera/camera_avfoundation/CHANGELOG.md | 4 + .../example/integration_test/camera_test.dart | 45 +++ .../ios/Runner.xcodeproj/project.pbxproj | 2 +- .../example/ios/Runner/Info.plist | 2 + .../example/ios/RunnerTests/CameraTestUtils.m | 13 +- .../example/lib/camera_controller.dart | 30 +- .../camera_avfoundation/example/lib/main.dart | 23 +- .../camera_avfoundation/example/pubspec.yaml | 8 +- .../ios/Classes/CameraPlugin.m | 2 + .../camera_avfoundation/ios/Classes/FLTCam.h | 2 + .../camera_avfoundation/ios/Classes/FLTCam.m | 177 ++++++--- .../ios/Classes/FLTCam_Test.h | 3 +- .../lib/src/avfoundation_camera.dart | 11 + .../camera/camera_avfoundation/pubspec.yaml | 4 +- .../test/avfoundation_camera_test.dart | 23 ++ 36 files changed, 1181 insertions(+), 190 deletions(-) create mode 100644 packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 13c00402449..ce75779da3b 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.4 + +* Allows camera to be switched while video recording. + ## 0.10.3 * Adds back use of Optional type. diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md index 86b0355b8bc..709b5913da2 100644 --- a/packages/camera/camera/README.md +++ b/packages/camera/camera/README.md @@ -75,7 +75,7 @@ void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.inactive) { cameraController.dispose(); } else if (state == AppLifecycleState.resumed) { - onNewCameraSelected(cameraController.description); + _initializeCameraController(cameraController.description); } } ``` diff --git a/packages/camera/camera/example/integration_test/camera_test.dart b/packages/camera/camera/example/integration_test/camera_test.dart index f0cc67f0c06..5338270828e 100644 --- a/packages/camera/camera/example/integration_test/camera_test.dart +++ b/packages/camera/camera/example/integration_test/camera_test.dart @@ -265,6 +265,49 @@ void main() { return completer.future; } + testWidgets('Set description while recording', (WidgetTester tester) async { + final List cameras = await availableCameras(); + if (cameras.length < 2) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + await controller.prepareForVideoRecording(); + + await controller.startVideoRecording(); + sleep(const Duration(milliseconds: 500)); + await controller.setDescription(cameras[1]); + sleep(const Duration(milliseconds: 500)); + + expect(controller.description, cameras[1]); + }); + + testWidgets('Set description', (WidgetTester tester) async { + final List cameras = await availableCameras(); + if (cameras.length < 2) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + sleep(const Duration(milliseconds: 500)); + await controller.setDescription(cameras[1]); + sleep(const Duration(milliseconds: 500)); + + expect(controller.description, cameras[1]); + }); + testWidgets( 'iOS image streaming with imageFormatGroup', (WidgetTester tester) async { diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index b343b6da9d8..e3e947c9113 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -120,7 +120,7 @@ class _CameraExampleHomeState extends State if (state == AppLifecycleState.inactive) { cameraController.dispose(); } else if (state == AppLifecycleState.resumed) { - onNewCameraSelected(cameraController.description); + _initializeCameraController(cameraController.description); } } // #enddocregion AppLifecycle @@ -597,10 +597,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, ), ), ); @@ -633,17 +630,15 @@ 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, kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium, diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 7a396c1589f..69917d3f039 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/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( @@ -188,6 +194,7 @@ class CameraValue { ? this.recordingOrientation : recordingOrientation.orNull, isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, + description: description ?? this.description, previewPauseOrientation: previewPauseOrientation == null ? this.previewPauseOrientation : previewPauseOrientation.orNull, @@ -211,7 +218,8 @@ class CameraValue { 'lockedCaptureOrientation: $lockedCaptureOrientation, ' 'recordingOrientation: $recordingOrientation, ' 'isPreviewPaused: $isPreviewPaused, ' - 'previewPausedOrientation: $previewPauseOrientation)'; + 'previewPausedOrientation: $previewPauseOrientation, ' + 'description: $description)'; } } @@ -225,14 +233,14 @@ class CameraValue { class CameraController extends ValueNotifier { /// Creates a new camera controller in an uninitialized state. CameraController( - this.description, + CameraDescription description, this.resolutionPreset, { this.enableAudio = true, 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 resolution this controller is targeting. /// @@ -274,7 +282,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', @@ -313,6 +326,7 @@ class CameraController extends ValueNotifier { value = value.copyWith( isInitialized: true, + description: description, previewSize: await initializeCompleter.future .then((CameraInitializedEvent event) => Size( event.previewWidth, @@ -380,6 +394,18 @@ class CameraController extends ValueNotifier { } } + /// Sets the description of the camera. + /// + /// 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); + } + } + /// Captures an image and returns the file where it was saved. /// /// Throws a [CameraException] if the capture fails. diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 1b902ab61f0..12ed3f80aa1 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.3 +version: 0.10.4 environment: sdk: ">=2.14.0 <3.0.0" @@ -23,7 +23,7 @@ flutter: dependencies: camera_android: ^0.10.1 camera_avfoundation: ^0.9.9 - camera_platform_interface: ^2.3.2 + camera_platform_interface: ^2.4.0 camera_web: ^0.3.1 flutter: sdk: flutter @@ -38,3 +38,11 @@ dev_dependencies: mockito: ^5.0.0 plugin_platform_interface: ^2.0.0 video_player: ^2.0.0 + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + camera_android: + path: ../../camera/camera_android + camera_avfoundation: + path: ../../camera/camera_avfoundation diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index 6677fcf9039..c73e1816445 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -11,7 +11,10 @@ import 'package:flutter_test/flutter_test.dart'; class FakeController extends ValueNotifier implements CameraController { - FakeController() : super(const CameraValue.uninitialized()); + FakeController() : super(const CameraValue.uninitialized(fakeDescription)); + + static const CameraDescription fakeDescription = CameraDescription( + name: '', lensDirection: CameraLensDirection.back, sensorOrientation: 0); @override Future dispose() async { @@ -29,10 +32,6 @@ class FakeController extends ValueNotifier @override void debugCheckIsDisposed() {} - @override - CameraDescription get description => const CameraDescription( - name: '', lensDirection: CameraLensDirection.back, sensorOrientation: 0); - @override bool get enableAudio => false; @@ -117,6 +116,12 @@ class FakeController extends ValueNotifier @override Future resumePreview() async {} + + @override + Future setDescription(CameraDescription description) async {} + + @override + CameraDescription get description => value.description; } void main() { diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index 37168dbd48d..036b8d7f4a0 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -13,26 +13,28 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'camera_preview_test.dart'; + void main() { group('camera_value', () { test('Can be created', () { const CameraValue cameraValue = CameraValue( - isInitialized: false, - previewSize: Size(10, 10), - isRecordingPaused: false, - isRecordingVideo: false, - isTakingPicture: false, - isStreamingImages: false, - flashMode: FlashMode.auto, - exposureMode: ExposureMode.auto, - exposurePointSupported: true, - focusMode: FocusMode.auto, - deviceOrientation: DeviceOrientation.portraitUp, - lockedCaptureOrientation: DeviceOrientation.portraitUp, - recordingOrientation: DeviceOrientation.portraitUp, - focusPointSupported: true, - previewPauseOrientation: DeviceOrientation.portraitUp, - ); + isInitialized: false, + previewSize: Size(10, 10), + isRecordingPaused: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + exposurePointSupported: true, + focusMode: FocusMode.auto, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: DeviceOrientation.portraitUp, + recordingOrientation: DeviceOrientation.portraitUp, + focusPointSupported: true, + previewPauseOrientation: DeviceOrientation.portraitUp, + description: FakeController.fakeDescription); expect(cameraValue, isA()); expect(cameraValue.isInitialized, isFalse); @@ -54,7 +56,8 @@ void main() { }); test('Can be created as uninitialized', () { - const CameraValue cameraValue = CameraValue.uninitialized(); + const CameraValue cameraValue = + CameraValue.uninitialized(FakeController.fakeDescription); expect(cameraValue, isA()); expect(cameraValue.isInitialized, isFalse); @@ -76,7 +79,8 @@ void main() { }); test('Can be copied with isInitialized', () { - const CameraValue cv = CameraValue.uninitialized(); + const CameraValue cv = + CameraValue.uninitialized(FakeController.fakeDescription); final CameraValue cameraValue = cv.copyWith(isInitialized: true); expect(cameraValue, isA()); @@ -99,7 +103,8 @@ void main() { }); test('Has aspectRatio after setting size', () { - const CameraValue cv = CameraValue.uninitialized(); + const CameraValue cv = + CameraValue.uninitialized(FakeController.fakeDescription); final CameraValue cameraValue = cv.copyWith(isInitialized: true, previewSize: const Size(20, 10)); @@ -107,7 +112,8 @@ void main() { }); test('hasError is true after setting errorDescription', () { - const CameraValue cv = CameraValue.uninitialized(); + const CameraValue cv = + CameraValue.uninitialized(FakeController.fakeDescription); final CameraValue cameraValue = cv.copyWith(errorDescription: 'error'); expect(cameraValue.hasError, isTrue); @@ -115,7 +121,8 @@ void main() { }); test('Recording paused is false when not recording', () { - const CameraValue cv = CameraValue.uninitialized(); + const CameraValue cv = + CameraValue.uninitialized(FakeController.fakeDescription); final CameraValue cameraValue = cv.copyWith( isInitialized: true, isRecordingVideo: false, @@ -126,25 +133,27 @@ void main() { test('toString() works as expected', () { const CameraValue cameraValue = CameraValue( - isInitialized: false, - previewSize: Size(10, 10), - isRecordingPaused: false, - isRecordingVideo: false, - isTakingPicture: false, - isStreamingImages: false, - flashMode: FlashMode.auto, - exposureMode: ExposureMode.auto, - focusMode: FocusMode.auto, - exposurePointSupported: true, - focusPointSupported: true, - deviceOrientation: DeviceOrientation.portraitUp, - lockedCaptureOrientation: DeviceOrientation.portraitUp, - recordingOrientation: DeviceOrientation.portraitUp, - isPreviewPaused: true, - previewPauseOrientation: DeviceOrientation.portraitUp); + isInitialized: false, + previewSize: Size(10, 10), + isRecordingPaused: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + focusMode: FocusMode.auto, + exposurePointSupported: true, + focusPointSupported: true, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: DeviceOrientation.portraitUp, + recordingOrientation: DeviceOrientation.portraitUp, + isPreviewPaused: true, + previewPauseOrientation: DeviceOrientation.portraitUp, + description: FakeController.fakeDescription, + ); expect(cameraValue.toString(), - 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, isPreviewPaused: true, previewPausedOrientation: DeviceOrientation.portraitUp)'); + 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, isPreviewPaused: true, previewPausedOrientation: DeviceOrientation.portraitUp, description: CameraDescription(, CameraLensDirection.back, 0))'); }); }); } diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md index 4609b402058..f7f0b2a0343 100644 --- a/packages/camera/camera_android/CHANGELOG.md +++ b/packages/camera/camera_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.5 + +* Allows camera to be switched while video recording. + ## 0.10.4 * Temporarily fixes issue with requested video profiles being null by falling back to deprecated behavior in that case. 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 b02d6864b5b..c2255e23273 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 @@ -115,13 +115,28 @@ class Camera * Holds all of the camera features/settings and will be used to update the request builder when * one changes. */ - private final CameraFeatures cameraFeatures; + private CameraFeatures cameraFeatures; + + private String imageFormatGroup; + + /** + * Takes an input/output surface and orients the recording correctly. This is needed because + * switching cameras while recording causes the wrong orientation. + */ + private VideoRenderer videoRenderer; + + /** + * Whether or not the camera aligns with the initial way the camera was facing if the camera was + * flipped. + */ + private int initialCameraFacing; private final SurfaceTextureEntry flutterTexture; + private final ResolutionPreset resolutionPreset; private final boolean enableAudio; private final Context applicationContext; private final DartMessenger dartMessenger; - private final CameraProperties cameraProperties; + private CameraProperties cameraProperties; private final CameraFeatureFactory cameraFeatureFactory; private final Activity activity; /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ @@ -211,6 +226,7 @@ public Camera( this.applicationContext = activity.getApplicationContext(); this.cameraProperties = cameraProperties; this.cameraFeatureFactory = cameraFeatureFactory; + this.resolutionPreset = resolutionPreset; this.cameraFeatures = CameraFeatures.init( cameraFeatureFactory, cameraProperties, activity, dartMessenger, resolutionPreset); @@ -251,6 +267,7 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { if (mediaRecorder != null) { mediaRecorder.release(); } + closeRenderer(); final PlatformChannel.DeviceOrientation lockedOrientation = ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) @@ -279,6 +296,7 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { @SuppressLint("MissingPermission") public void open(String imageFormatGroup) throws CameraAccessException { + this.imageFormatGroup = imageFormatGroup; final ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); if (!resolutionFeature.checkIsSupported()) { @@ -323,14 +341,16 @@ public void onOpened(@NonNull CameraDevice device) { cameraDevice = new DefaultCameraDeviceWrapper(device); try { startPreview(); + if (!recordingVideo) // only send initialization if we werent already recording and switching cameras dartMessenger.sendCameraInitializedEvent( - resolutionFeature.getPreviewSize().getWidth(), - resolutionFeature.getPreviewSize().getHeight(), - cameraFeatures.getExposureLock().getValue(), - cameraFeatures.getAutoFocus().getValue(), - cameraFeatures.getExposurePoint().checkIsSupported(), - cameraFeatures.getFocusPoint().checkIsSupported()); - } catch (CameraAccessException e) { + resolutionFeature.getPreviewSize().getWidth(), + resolutionFeature.getPreviewSize().getHeight(), + cameraFeatures.getExposureLock().getValue(), + cameraFeatures.getAutoFocus().getValue(), + cameraFeatures.getExposurePoint().checkIsSupported(), + cameraFeatures.getFocusPoint().checkIsSupported()); + + } catch (CameraAccessException | InterruptedException e) { dartMessenger.sendCameraErrorEvent(e.getMessage()); close(); } @@ -340,7 +360,8 @@ public void onOpened(@NonNull CameraDevice device) { public void onClosed(@NonNull CameraDevice camera) { Log.i(TAG, "open | onClosed"); - // Prevents calls to methods that would otherwise result in IllegalStateException exceptions. + // Prevents calls to methods that would otherwise result in IllegalStateException + // exceptions. cameraDevice = null; closeCaptureSession(); dartMessenger.sendCameraClosingEvent(); @@ -756,7 +777,7 @@ public void startVideoRecording( if (imageStreamChannel != null) { setStreamHandler(imageStreamChannel); } - + initialCameraFacing = cameraProperties.getLensFacing(); recordingVideo = true; try { startCapture(true, imageStreamChannel != null); @@ -768,6 +789,13 @@ public void startVideoRecording( } } + private void closeRenderer() { + if (videoRenderer != null) { + videoRenderer.close(); + videoRenderer = null; + } + } + public void stopVideoRecording(@NonNull final Result result) { if (!recordingVideo) { result.success(null); @@ -778,6 +806,7 @@ public void stopVideoRecording(@NonNull final Result result) { cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false)); recordingVideo = false; try { + closeRenderer(); captureSession.abortCaptures(); mediaRecorder.stop(); } catch (CameraAccessException | IllegalStateException e) { @@ -786,7 +815,7 @@ public void stopVideoRecording(@NonNull final Result result) { mediaRecorder.reset(); try { startPreview(); - } catch (CameraAccessException | IllegalStateException e) { + } catch (CameraAccessException | IllegalStateException | InterruptedException e) { result.error("videoRecordingFailed", e.getMessage(), null); return; } @@ -1070,13 +1099,51 @@ public void resumePreview() { null, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); } - public void startPreview() throws CameraAccessException { + public void startPreview() throws CameraAccessException, InterruptedException { + // If recording is already in progress, the camera is being flipped, so send it through the VideoRenderer to keep the correct orientation. + if (recordingVideo) { + startPreviewWithVideoRendererStream(); + } else { + startRegularPreview(); + } + } + + private void startRegularPreview() throws CameraAccessException { if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; Log.i(TAG, "startPreview"); - createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); } + private void startPreviewWithVideoRendererStream() + throws CameraAccessException, InterruptedException { + if (videoRenderer == null) return; + + // get rotation for rendered video + final PlatformChannel.DeviceOrientation lockedOrientation = + ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) + .getLockedCaptureOrientation(); + DeviceOrientationManager orientationManager = + cameraFeatures.getSensorOrientation().getDeviceOrientationManager(); + + int rotation = 0; + if (orientationManager != null) { + rotation = + lockedOrientation == null + ? orientationManager.getVideoOrientation() + : orientationManager.getVideoOrientation(lockedOrientation); + } + + if (cameraProperties.getLensFacing() != initialCameraFacing) { + + // If the new camera is facing the opposite way than the initial recording, + // the rotation should be flipped 180 degrees. + rotation = (rotation + 180) % 360; + } + videoRenderer.setRotation(rotation); + + createCaptureSession(CameraDevice.TEMPLATE_RECORD, videoRenderer.getInputSurface()); + } + public void startPreviewWithImageStream(EventChannel imageStreamChannel) throws CameraAccessException { setStreamHandler(imageStreamChannel); @@ -1200,17 +1267,7 @@ private void closeCaptureSession() { public void close() { Log.i(TAG, "close"); - if (cameraDevice != null) { - cameraDevice.close(); - cameraDevice = null; - - // Closing the CameraDevice without closing the CameraCaptureSession is recommended - // for quickly closing the camera: - // https://developer.android.com/reference/android/hardware/camera2/CameraCaptureSession#close() - captureSession = null; - } else { - closeCaptureSession(); - } + stopAndReleaseCamera(); if (pictureImageReader != null) { pictureImageReader.close(); @@ -1229,6 +1286,66 @@ public void close() { stopBackgroundThread(); } + private void stopAndReleaseCamera() { + if (cameraDevice != null) { + cameraDevice.close(); + cameraDevice = null; + + // Closing the CameraDevice without closing the CameraCaptureSession is recommended + // for quickly closing the camera: + // https://developer.android.com/reference/android/hardware/camera2/CameraCaptureSession#close() + captureSession = null; + } else { + closeCaptureSession(); + } + } + + private void prepareVideoRenderer() { + if (videoRenderer != null) return; + final ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); + + // handle videoRenderer errors + Thread.UncaughtExceptionHandler videoRendererUncaughtExceptionHandler = + new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread thread, Throwable ex) { + dartMessenger.sendCameraErrorEvent( + "Failed to process frames after camera was flipped."); + } + }; + + videoRenderer = + new VideoRenderer( + mediaRecorder.getSurface(), + resolutionFeature.getCaptureSize().getWidth(), + resolutionFeature.getCaptureSize().getHeight(), + videoRendererUncaughtExceptionHandler); + } + + public void setDescriptionWhileRecording( + @NonNull final Result result, CameraProperties properties) { + + if (!recordingVideo) { + result.error("setDescriptionWhileRecordingFailed", "Device was not recording", null); + return; + } + + stopAndReleaseCamera(); + prepareVideoRenderer(); + cameraProperties = properties; + cameraFeatures = + CameraFeatures.init( + cameraFeatureFactory, cameraProperties, activity, dartMessenger, resolutionPreset); + cameraFeatures.setAutoFocus( + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true)); + try { + open(imageFormatGroup); + } catch (CameraAccessException e) { + result.error("setDescriptionWhileRecordingFailed", e.getMessage(), null); + } + result.success(null); + } + public void dispose() { Log.i(TAG, "dispose"); 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 432344ade8c..aad62bbaba8 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 @@ -354,6 +354,18 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) result.success(null); break; } + case "setDescriptionWhileRecording": + { + try { + String cameraName = call.argument("cameraName"); + CameraProperties cameraProperties = + new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity)); + camera.setDescriptionWhileRecording(result, cameraProperties); + } catch (Exception e) { + handleException(e, result); + } + break; + } case "dispose": { if (camera != null) { diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java new file mode 100644 index 00000000000..b7128373b10 --- /dev/null +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java @@ -0,0 +1,365 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static android.os.SystemClock.uptimeMillis; + +import android.graphics.SurfaceTexture; +import android.opengl.EGL14; +import android.opengl.EGLConfig; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLExt; +import android.opengl.EGLSurface; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.opengl.Matrix; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; +import android.view.Surface; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Renders video onto texture after performing a matrix rotation on each frame. + * + *

VideoRenderer is needed because when switching between cameras mid recording, the orientation + * of the recording from the new camera usually becomes flipped. MediaRecorder has + * setOrientationHint, but that cannot be called mid recording and therefore isn't useful. Android + * Camera2 has no setDisplayOrientation on the camera itself as it is supposed to 'just work' (see + * https://stackoverflow.com/questions/33479004/what-is-the-camera2-api-equivalent-of-setdisplayorientation). + * Therefore it cannot be used to set the camera's orientation either. + * + *

This leaves the solution to be routing the recording through a surface texture and performing + * a matrix transformation on it manually to get the correct orientation. This only happens when + * setDescription is called mid video recording. + */ +public class VideoRenderer { + + private static String TAG = "VideoRenderer"; + + private static final String vertexShaderCode = + " precision highp float;\n" + + " attribute vec3 vertexPosition;\n" + + " attribute vec2 uvs;\n" + + " varying vec2 varUvs;\n" + + " uniform mat4 texMatrix;\n" + + " uniform mat4 mvp;\n" + + "\n" + + " void main()\n" + + " {\n" + + " varUvs = (texMatrix * vec4(uvs.x, uvs.y, 0, 1.0)).xy;\n" + + " gl_Position = mvp * vec4(vertexPosition, 1.0);\n" + + " }"; + + private static final String fragmentShaderCode = + " #extension GL_OES_EGL_image_external : require\n" + + " precision mediump float;\n" + + "\n" + + " varying vec2 varUvs;\n" + + " uniform samplerExternalOES texSampler;\n" + + "\n" + + " void main()\n" + + " {\n" + + " vec4 c = texture2D(texSampler, varUvs);\n" + + " gl_FragColor = vec4(c.r, c.g, c.b, c.a);\n" + + " }"; + + private final int[] textureHandles = new int[1]; + + private final float[] vertices = + new float[] { + -1.0f, -1.0f, 0.0f, 0f, 0f, -1.0f, 1.0f, 0.0f, 0f, 1f, 1.0f, 1.0f, 0.0f, 1f, 1f, 1.0f, + -1.0f, 0.0f, 1f, 0f + }; + + private final int[] indices = new int[] {2, 1, 0, 0, 3, 2}; + + private int program; + private int vertexHandle = 0; + private final int[] bufferHandles = new int[2]; + private int uvsHandle = 0; + private int texMatrixHandle = 0; + private int mvpHandle = 0; + + EGLDisplay display; + EGLContext context; + EGLSurface surface; + private Thread thread; + private final Surface outputSurface; + private SurfaceTexture inputSurfaceTexture; + private Surface inputSurface; + + private HandlerThread surfaceTextureFrameAvailableHandler; + private final Object surfaceTextureAvailableFrameLock = new Object(); + private Boolean surfaceTextureFrameAvailable = false; + + private final int recordingWidth; + private final int recordingHeight; + private int rotation = 0; + + private final Object lock = new Object(); + + private final Thread.UncaughtExceptionHandler uncaughtExceptionHandler; + + /** Gets surface for input. Blocks until surface is ready. */ + public Surface getInputSurface() throws InterruptedException { + synchronized (lock) { + while (inputSurface == null) { + lock.wait(); + } + } + return inputSurface; + } + + public VideoRenderer( + Surface outputSurface, + int recordingWidth, + int recordingHeight, + Thread.UncaughtExceptionHandler uncaughtExceptionHandler) { + this.outputSurface = outputSurface; + this.recordingHeight = recordingHeight; + this.recordingWidth = recordingWidth; + this.uncaughtExceptionHandler = uncaughtExceptionHandler; + startOpenGL(); + Log.d(TAG, "VideoRenderer setup complete"); + } + + /** Stop rendering and cleanup resources. */ + public void close() { + thread.interrupt(); + surfaceTextureFrameAvailableHandler.quitSafely(); + cleanupOpenGL(); + inputSurfaceTexture.release(); + } + + private void cleanupOpenGL() { + GLES20.glDeleteBuffers(2, bufferHandles, 0); + GLES20.glDeleteTextures(1, textureHandles, 0); + EGL14.eglDestroyContext(display, context); + EGL14.eglDestroySurface(display, surface); + GLES20.glDeleteProgram(program); + } + + /** Configures openGL. Must be called in same thread as draw is called. */ + private void configureOpenGL() { + synchronized (lock) { + display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + if (display == EGL14.EGL_NO_DISPLAY) + throw new RuntimeException( + "eglDisplay == EGL14.EGL_NO_DISPLAY: " + + GLUtils.getEGLErrorString(EGL14.eglGetError())); + + int[] version = new int[2]; + if (!EGL14.eglInitialize(display, version, 0, version, 1)) + throw new RuntimeException( + "eglInitialize(): " + GLUtils.getEGLErrorString(EGL14.eglGetError())); + + String eglExtensions = EGL14.eglQueryString(display, EGL14.EGL_EXTENSIONS); + if (!eglExtensions.contains("EGL_ANDROID_presentation_time")) + throw new RuntimeException( + "cannot configure OpenGL. missing EGL_ANDROID_presentation_time"); + + int[] attribList = + new int[] { + EGL14.EGL_RED_SIZE, 8, + EGL14.EGL_GREEN_SIZE, 8, + EGL14.EGL_BLUE_SIZE, 8, + EGL14.EGL_ALPHA_SIZE, 8, + EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, + EGLExt.EGL_RECORDABLE_ANDROID, 1, + EGL14.EGL_NONE + }; + + EGLConfig[] configs = new EGLConfig[1]; + int[] numConfigs = new int[1]; + if (!EGL14.eglChooseConfig(display, attribList, 0, configs, 0, configs.length, numConfigs, 0)) + throw new RuntimeException(GLUtils.getEGLErrorString(EGL14.eglGetError())); + + int err = EGL14.eglGetError(); + if (err != EGL14.EGL_SUCCESS) throw new RuntimeException(GLUtils.getEGLErrorString(err)); + + int[] ctxAttribs = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE}; + context = EGL14.eglCreateContext(display, configs[0], EGL14.EGL_NO_CONTEXT, ctxAttribs, 0); + + err = EGL14.eglGetError(); + if (err != EGL14.EGL_SUCCESS) throw new RuntimeException(GLUtils.getEGLErrorString(err)); + + int[] surfaceAttribs = new int[] {EGL14.EGL_NONE}; + + surface = EGL14.eglCreateWindowSurface(display, configs[0], outputSurface, surfaceAttribs, 0); + + err = EGL14.eglGetError(); + if (err != EGL14.EGL_SUCCESS) throw new RuntimeException(GLUtils.getEGLErrorString(err)); + + if (!EGL14.eglMakeCurrent(display, surface, surface, context)) + throw new RuntimeException( + "eglMakeCurrent(): " + GLUtils.getEGLErrorString(EGL14.eglGetError())); + + ByteBuffer vertexBuffer = ByteBuffer.allocateDirect(vertices.length * 4); + vertexBuffer.order(ByteOrder.nativeOrder()); + vertexBuffer.asFloatBuffer().put(vertices); + vertexBuffer.asFloatBuffer().position(0); + + ByteBuffer indexBuffer = ByteBuffer.allocateDirect(indices.length * 4); + indexBuffer.order(ByteOrder.nativeOrder()); + indexBuffer.asIntBuffer().put(indices); + indexBuffer.position(0); + + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode); + int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode); + + program = GLES20.glCreateProgram(); + + GLES20.glAttachShader(program, vertexShader); + GLES20.glAttachShader(program, fragmentShader); + GLES20.glLinkProgram(program); + + deleteShader(vertexShader); + deleteShader(fragmentShader); + + vertexHandle = GLES20.glGetAttribLocation(program, "vertexPosition"); + uvsHandle = GLES20.glGetAttribLocation(program, "uvs"); + texMatrixHandle = GLES20.glGetUniformLocation(program, "texMatrix"); + mvpHandle = GLES20.glGetUniformLocation(program, "mvp"); + + // Initialize buffers + GLES20.glGenBuffers(2, bufferHandles, 0); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferHandles[0]); + GLES20.glBufferData( + GLES20.GL_ARRAY_BUFFER, vertices.length * 4, vertexBuffer, GLES20.GL_DYNAMIC_DRAW); + + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, bufferHandles[1]); + GLES20.glBufferData( + GLES20.GL_ELEMENT_ARRAY_BUFFER, indices.length * 4, indexBuffer, GLES20.GL_DYNAMIC_DRAW); + + // Init texture that will receive decoded frames + GLES20.glGenTextures(1, textureHandles, 0); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureHandles[0]); + + inputSurfaceTexture = new SurfaceTexture(getTexId()); + inputSurfaceTexture.setDefaultBufferSize(recordingWidth, recordingHeight); + surfaceTextureFrameAvailableHandler = new HandlerThread("FrameHandlerThread"); + surfaceTextureFrameAvailableHandler.start(); + inputSurface = new Surface(inputSurfaceTexture); + + inputSurfaceTexture.setOnFrameAvailableListener( + new SurfaceTexture.OnFrameAvailableListener() { + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + synchronized (surfaceTextureAvailableFrameLock) { + if (surfaceTextureFrameAvailable) + Log.w(TAG, "Frame available before processing other frames. dropping frames"); + surfaceTextureFrameAvailable = true; + surfaceTextureAvailableFrameLock.notifyAll(); + } + } + }, + new Handler(surfaceTextureFrameAvailableHandler.getLooper())); + lock.notifyAll(); + } + } + + /** Starts and configures Video Renderer. */ + private void startOpenGL() { + Log.d(TAG, "Starting OpenGL Thread"); + thread = + new Thread() { + @Override + public void run() { + + configureOpenGL(); + + try { + // Continuously pull frames from input surface texture and use videoRenderer to modify + // to correct rotation. + while (!Thread.interrupted()) { + + synchronized (surfaceTextureAvailableFrameLock) { + while (!surfaceTextureFrameAvailable) { + surfaceTextureAvailableFrameLock.wait(500); + } + surfaceTextureFrameAvailable = false; + } + + inputSurfaceTexture.updateTexImage(); + + float[] surfaceTextureMatrix = new float[16]; + inputSurfaceTexture.getTransformMatrix(surfaceTextureMatrix); + + draw(recordingWidth, recordingHeight, surfaceTextureMatrix); + } + } catch (InterruptedException e) { + Log.d(TAG, "thread interrupted while waiting for frames"); + } + } + }; + thread.setUncaughtExceptionHandler(uncaughtExceptionHandler); + thread.start(); + } + + public int getTexId() { + return textureHandles[0]; + } + + public float[] moveMatrix() { + float[] m = new float[16]; + Matrix.setIdentityM(m, 0); + Matrix.rotateM(m, 0, rotation, 0, 0, 1); + return m; + } + + public void setRotation(int rotation) { + this.rotation = rotation; + } + + private int loadShader(int type, String code) { + + int shader = GLES20.glCreateShader(type); + + GLES20.glShaderSource(shader, code); + GLES20.glCompileShader(shader); + return shader; + } + + private void deleteShader(int shader) { + GLES20.glDeleteShader(shader); + } + + public void draw(int viewportWidth, int viewportHeight, float[] texMatrix) { + + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); + GLES20.glClearColor(0f, 0f, 0f, 0f); + + GLES20.glViewport(0, 0, viewportWidth, viewportHeight); + + GLES20.glUseProgram(program); + + // Pass transformations to shader + GLES20.glUniformMatrix4fv(texMatrixHandle, 1, false, texMatrix, 0); + GLES20.glUniformMatrix4fv(mvpHandle, 1, false, moveMatrix(), 0); + + // Prepare buffers with vertices and indices & draw + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferHandles[0]); + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, bufferHandles[1]); + + GLES20.glEnableVertexAttribArray(vertexHandle); + GLES20.glVertexAttribPointer(vertexHandle, 3, GLES20.GL_FLOAT, false, 4 * 5, 0); + + GLES20.glEnableVertexAttribArray(uvsHandle); + GLES20.glVertexAttribPointer(uvsHandle, 2, GLES20.GL_FLOAT, false, 4 * 5, 3 * 4); + + GLES20.glDrawElements(GLES20.GL_TRIANGLES, 6, GLES20.GL_UNSIGNED_INT, 0); + + EGLExt.eglPresentationTimeANDROID(display, surface, uptimeMillis() * 1000000); + if (!EGL14.eglSwapBuffers(display, surface)) { + throw new RuntimeException( + "eglSwapBuffers()" + GLUtils.getEGLErrorString(EGL14.eglGetError())); + } + } +} diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java index 9a679017ded..9a6f7dc20d2 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -602,6 +602,115 @@ public void resumeVideoRecording_shouldCallPauseWhenRecordingAndOnAPIN() { verify(mockResult, never()).error(any(), any(), any()); } + @Test + public void setDescriptionWhileRecording() { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + VideoRenderer mockVideoRenderer = mock(VideoRenderer.class); + TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setPrivateField(camera, "videoRenderer", mockVideoRenderer); + + final CameraProperties newCameraProperties = mock(CameraProperties.class); + camera.setDescriptionWhileRecording(mockResult, newCameraProperties); + + verify(mockResult, times(1)).success(null); + verify(mockResult, never()).error(any(), any(), any()); + } + + @Test + public void startPreview_shouldPullStreamFromVideoRenderer() + throws InterruptedException, CameraAccessException { + VideoRenderer mockVideoRenderer = mock(VideoRenderer.class); + ArrayList mockRequestBuilders = new ArrayList<>(); + mockRequestBuilders.add(mock(CaptureRequest.Builder.class)); + SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class); + Size mockSize = mock(Size.class); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setPrivateField(camera, "videoRenderer", mockVideoRenderer); + CameraDeviceWrapper fakeCamera = new FakeCameraDeviceWrapper(mockRequestBuilders); + TestUtils.setPrivateField(camera, "cameraDevice", fakeCamera); + + TextureRegistry.SurfaceTextureEntry cameraFlutterTexture = + (TextureRegistry.SurfaceTextureEntry) TestUtils.getPrivateField(camera, "flutterTexture"); + ResolutionFeature resolutionFeature = + (ResolutionFeature) + TestUtils.getPrivateField(mockCameraFeatureFactory, "mockResolutionFeature"); + + when(cameraFlutterTexture.surfaceTexture()).thenReturn(mockSurfaceTexture); + when(resolutionFeature.getPreviewSize()).thenReturn(mockSize); + + camera.startPreview(); + verify(mockVideoRenderer, times(1)) + .getInputSurface(); // stream pulled from videoRenderer's surface. + } + + @Test + public void startPreview_shouldPullStreamFromImageReader() + throws InterruptedException, CameraAccessException { + ArrayList mockRequestBuilders = new ArrayList<>(); + mockRequestBuilders.add(mock(CaptureRequest.Builder.class)); + SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class); + Size mockSize = mock(Size.class); + ImageReader mockImageReader = mock(ImageReader.class); + TestUtils.setPrivateField(camera, "recordingVideo", false); + TestUtils.setPrivateField(camera, "pictureImageReader", mockImageReader); + CameraDeviceWrapper fakeCamera = new FakeCameraDeviceWrapper(mockRequestBuilders); + TestUtils.setPrivateField(camera, "cameraDevice", fakeCamera); + + TextureRegistry.SurfaceTextureEntry cameraFlutterTexture = + (TextureRegistry.SurfaceTextureEntry) TestUtils.getPrivateField(camera, "flutterTexture"); + ResolutionFeature resolutionFeature = + (ResolutionFeature) + TestUtils.getPrivateField(mockCameraFeatureFactory, "mockResolutionFeature"); + + when(cameraFlutterTexture.surfaceTexture()).thenReturn(mockSurfaceTexture); + when(resolutionFeature.getPreviewSize()).thenReturn(mockSize); + + camera.startPreview(); + verify(mockImageReader, times(1)) + .getSurface(); // stream pulled from regular imageReader's surface. + } + + @Test + public void startPreview_shouldFlipRotation() throws InterruptedException, CameraAccessException { + VideoRenderer mockVideoRenderer = mock(VideoRenderer.class); + ArrayList mockRequestBuilders = new ArrayList<>(); + mockRequestBuilders.add(mock(CaptureRequest.Builder.class)); + SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class); + Size mockSize = mock(Size.class); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setPrivateField(camera, "videoRenderer", mockVideoRenderer); + TestUtils.setPrivateField(camera, "initialCameraFacing", CameraMetadata.LENS_FACING_BACK); + CameraDeviceWrapper fakeCamera = new FakeCameraDeviceWrapper(mockRequestBuilders); + TestUtils.setPrivateField(camera, "cameraDevice", fakeCamera); + + TextureRegistry.SurfaceTextureEntry cameraFlutterTexture = + (TextureRegistry.SurfaceTextureEntry) TestUtils.getPrivateField(camera, "flutterTexture"); + ResolutionFeature resolutionFeature = + (ResolutionFeature) + TestUtils.getPrivateField(mockCameraFeatureFactory, "mockResolutionFeature"); + + when(cameraFlutterTexture.surfaceTexture()).thenReturn(mockSurfaceTexture); + when(resolutionFeature.getPreviewSize()).thenReturn(mockSize); + when(mockCameraProperties.getLensFacing()).thenReturn(CameraMetadata.LENS_FACING_FRONT); + + camera.startPreview(); + verify(mockVideoRenderer, times(1)).setRotation(180); + } + + @Test + public void setDescriptionWhileRecording_shouldErrorWhenNotRecording() { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + TestUtils.setPrivateField(camera, "recordingVideo", false); + final CameraProperties newCameraProperties = mock(CameraProperties.class); + camera.setDescriptionWhileRecording(mockResult, newCameraProperties); + + verify(mockResult, times(1)) + .error("setDescriptionWhileRecordingFailed", "Device was not recording", null); + verify(mockResult, never()).success(any()); + } + @Test public void resumeVideoRecording_shouldSendVideoRecordingFailedErrorWhenVersionCodeSmallerThanN() { 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 e499872da5f..517a50d02cc 100644 --- a/packages/camera/camera_android/example/integration_test/camera_test.dart +++ b/packages/camera/camera_android/example/integration_test/camera_test.dart @@ -205,6 +205,51 @@ void main() { expect(duration, lessThan(recordingTime - timePaused)); }); + testWidgets('Set description while recording', (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.length < 2) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + await controller.prepareForVideoRecording(); + + await controller.startVideoRecording(); + sleep(const Duration(milliseconds: 500)); + await controller.setDescription(cameras[1]); + sleep(const Duration(milliseconds: 500)); + + expect(controller.description, cameras[1]); + }); + + testWidgets('Set description', (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.length < 2) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + sleep(const Duration(milliseconds: 500)); + await controller.setDescription(cameras[1]); + sleep(const Duration(milliseconds: 500)); + + expect(controller.description, cameras[1]); + }); + testWidgets( 'image streaming', (WidgetTester tester) async { diff --git a/packages/camera/camera_android/example/lib/camera_controller.dart b/packages/camera/camera_android/example/lib/camera_controller.dart index 8139dcdb022..fd4f09a027b 100644 --- a/packages/camera/camera_android/example/lib/camera_controller.dart +++ b/packages/camera/camera_android/example/lib/camera_controller.dart @@ -24,6 +24,7 @@ class CameraValue { required this.exposureMode, required this.focusMode, required this.deviceOrientation, + required this.description, this.lockedCaptureOrientation, this.recordingOrientation, this.isPreviewPaused = false, @@ -31,7 +32,7 @@ class CameraValue { }); /// Creates a new camera controller state for an uninitialized controller. - const CameraValue.uninitialized() + const CameraValue.uninitialized(CameraDescription description) : this( isInitialized: false, isRecordingVideo: false, @@ -43,6 +44,7 @@ class CameraValue { focusMode: FocusMode.auto, deviceOrientation: DeviceOrientation.portraitUp, isPreviewPaused: false, + description: description, ); /// True after [CameraController.initialize] has completed successfully. @@ -92,6 +94,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 @@ -112,6 +117,7 @@ class CameraValue { Optional? lockedCaptureOrientation, Optional? recordingOrientation, bool? isPreviewPaused, + CameraDescription? description, Optional? previewPauseOrientation, }) { return CameraValue( @@ -132,6 +138,7 @@ class CameraValue { ? this.recordingOrientation : recordingOrientation.orNull, isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, + description: description ?? this.description, previewPauseOrientation: previewPauseOrientation == null ? this.previewPauseOrientation : previewPauseOrientation.orNull, @@ -165,14 +172,14 @@ class CameraValue { class CameraController extends ValueNotifier { /// Creates a new camera controller in an uninitialized state. CameraController( - this.description, + CameraDescription cameraDescription, this.resolutionPreset, { this.enableAudio = true, this.imageFormatGroup, - }) : super(const CameraValue.uninitialized()); + }) : super(CameraValue.uninitialized(cameraDescription)); /// The properties of the camera device controlled by this controller. - final CameraDescription description; + CameraDescription get description => value.description; /// The resolution this controller is targeting. /// @@ -202,7 +209,9 @@ class CameraController extends ValueNotifier { int get cameraId => _cameraId; /// Initializes the camera on the device. - Future initialize() async { + Future initialize() => _initializeWithDescription(description); + + Future _initializeWithDescription(CameraDescription description) async { final Completer initializeCompleter = Completer(); @@ -234,6 +243,7 @@ class CameraController extends ValueNotifier { value = value.copyWith( isInitialized: true, + description: description, previewSize: await initializeCompleter.future .then((CameraInitializedEvent event) => Size( event.previewWidth, @@ -274,6 +284,16 @@ class CameraController extends ValueNotifier { previewPauseOrientation: const Optional.absent()); } + /// Sets the description of the camera. + Future setDescription(CameraDescription description) async { + if (value.isRecordingVideo) { + await CameraPlatform.instance.setDescriptionWhileRecording(description); + value = value.copyWith(description: description); + } else { + await _initializeWithDescription(description); + } + } + /// Captures an image and returns the file where it was saved. /// /// Throws a [CameraException] if the capture fails. diff --git a/packages/camera/camera_android/example/lib/main.dart b/packages/camera/camera_android/example/lib/main.dart index 4d98aed9a4c..3731325a49f 100644 --- a/packages/camera/camera_android/example/lib/main.dart +++ b/packages/camera/camera_android/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); } } @@ -603,10 +603,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, ), ), ); @@ -639,17 +636,15 @@ 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, kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium, diff --git a/packages/camera/camera_android/example/pubspec.yaml b/packages/camera/camera_android/example/pubspec.yaml index e23e31a886d..7a802a2a3a2 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.3.1 + camera_platform_interface: ^2.4.0 flutter: sdk: flutter path_provider: ^2.0.0 @@ -32,3 +32,9 @@ dev_dependencies: flutter: uses-material-design: true + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + camera_android: + path: ../../../camera/camera_android diff --git a/packages/camera/camera_android/lib/src/android_camera.dart b/packages/camera/camera_android/lib/src/android_camera.dart index 9ab9b578616..eca1003247c 100644 --- a/packages/camera/camera_android/lib/src/android_camera.dart +++ b/packages/camera/camera_android/lib/src/android_camera.dart @@ -505,6 +505,17 @@ class AndroidCamera extends CameraPlatform { ); } + @override + Future setDescriptionWhileRecording( + CameraDescription description) async { + await _channel.invokeMethod( + 'setDescriptionWhileRecording', + { + 'cameraName': description.name, + }, + ); + } + @override Widget buildPreview(int cameraId) { return Texture(textureId: cameraId); diff --git a/packages/camera/camera_android/pubspec.yaml b/packages/camera/camera_android/pubspec.yaml index fb337191291..c6ce575f2a9 100644 --- a/packages/camera/camera_android/pubspec.yaml +++ b/packages/camera/camera_android/pubspec.yaml @@ -18,7 +18,7 @@ flutter: dartPluginClass: AndroidCamera dependencies: - camera_platform_interface: ^2.3.1 + camera_platform_interface: ^2.4.0 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 d80bd9cac7a..b56aa4e352a 100644 --- a/packages/camera/camera_android/test/android_camera_test.dart +++ b/packages/camera/camera_android/test/android_camera_test.dart @@ -700,6 +700,29 @@ void main() { ]); }); + test('Should set the description while recording', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setDescriptionWhileRecording': null}, + ); + const CameraDescription camera2Description = CameraDescription( + name: 'Test2', + lensDirection: CameraLensDirection.front, + sensorOrientation: 0); + + // Act + await camera.setDescriptionWhileRecording(camera2Description); + + // Assert + expect(channel.log, [ + isMethodCall('setDescriptionWhileRecording', + arguments: { + 'cameraName': camera2Description.name, + }), + ]); + }); + test('Should set the flash mode', () async { // Arrange final MethodChannelMock channel = MethodChannelMock( diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index f1496c64049..3365bf4120d 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -32,3 +32,4 @@ dev_dependencies: sdk: flutter mockito: ^5.3.2 pigeon: ^3.2.6 + diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index f0605b7914c..169596fa647 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.12 + +* Allows camera to be switched while video recording. + ## 0.9.11 * Adds back use of Optional type. 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 34d460d44ec..5a6935a9011 100644 --- a/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart +++ b/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart @@ -198,6 +198,51 @@ void main() { expect(duration, lessThan(recordingTime - timePaused)); }); + testWidgets('Set description while recording', (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.length < 2) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + await controller.prepareForVideoRecording(); + + await controller.startVideoRecording(); + sleep(const Duration(milliseconds: 500)); + await controller.setDescription(cameras[1]); + sleep(const Duration(milliseconds: 500)); + + expect(controller.description, cameras[1]); + }); + + testWidgets('Set description', (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.length < 2) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + sleep(const Duration(milliseconds: 500)); + await controller.setDescription(cameras[1]); + sleep(const Duration(milliseconds: 500)); + + expect(controller.description, cameras[1]); + }); + /// Start streaming with specifying the ImageFormatGroup. Future startStreaming(List cameras, ImageFormatGroup? imageFormatGroup) async { diff --git a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj index 03c80d79c57..c63d0086020 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist b/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist index ff2e341a180..c50ce989f0c 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist +++ b/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist @@ -52,5 +52,7 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.m index 0ae4887eb63..b42aa34e2a1 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.m @@ -11,15 +11,20 @@ OCMStub([inputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg setTo:nil]]) .andReturn(inputMock); - id sessionMock = OCMClassMock([AVCaptureSession class]); - OCMStub([sessionMock addInputWithNoConnections:[OCMArg any]]); // no-op - OCMStub([sessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); + id videoSessionMock = OCMClassMock([AVCaptureSession class]); + OCMStub([videoSessionMock addInputWithNoConnections:[OCMArg any]]); // no-op + OCMStub([videoSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); + + id audioSessionMock = OCMClassMock([AVCaptureSession class]); + OCMStub([audioSessionMock addInputWithNoConnections:[OCMArg any]]); // no-op + OCMStub([audioSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); return [[FLTCam alloc] initWithCameraName:@"camera" resolutionPreset:@"medium" enableAudio:true orientation:UIDeviceOrientationPortrait - captureSession:sessionMock + videoCaptureSession:videoSessionMock + audioCaptureSession:audioSessionMock captureSessionQueue:captureSessionQueue error:nil]; } diff --git a/packages/camera/camera_avfoundation/example/lib/camera_controller.dart b/packages/camera/camera_avfoundation/example/lib/camera_controller.dart index 524186816aa..6e1804328d5 100644 --- a/packages/camera/camera_avfoundation/example/lib/camera_controller.dart +++ b/packages/camera/camera_avfoundation/example/lib/camera_controller.dart @@ -24,6 +24,7 @@ class CameraValue { required this.exposureMode, required this.focusMode, required this.deviceOrientation, + required this.description, this.lockedCaptureOrientation, this.recordingOrientation, this.isPreviewPaused = false, @@ -31,7 +32,7 @@ class CameraValue { }); /// Creates a new camera controller state for an uninitialized controller. - const CameraValue.uninitialized() + const CameraValue.uninitialized(CameraDescription description) : this( isInitialized: false, isRecordingVideo: false, @@ -43,6 +44,7 @@ class CameraValue { focusMode: FocusMode.auto, deviceOrientation: DeviceOrientation.portraitUp, isPreviewPaused: false, + description: description, ); /// True after [CameraController.initialize] has completed successfully. @@ -92,6 +94,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 @@ -112,6 +117,7 @@ class CameraValue { Optional? lockedCaptureOrientation, Optional? recordingOrientation, bool? isPreviewPaused, + CameraDescription? description, Optional? previewPauseOrientation, }) { return CameraValue( @@ -132,6 +138,7 @@ class CameraValue { ? this.recordingOrientation : recordingOrientation.orNull, isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, + description: description ?? this.description, previewPauseOrientation: previewPauseOrientation == null ? this.previewPauseOrientation : previewPauseOrientation.orNull, @@ -165,14 +172,14 @@ class CameraValue { class CameraController extends ValueNotifier { /// Creates a new camera controller in an uninitialized state. CameraController( - this.description, + CameraDescription cameraDescription, this.resolutionPreset, { this.enableAudio = true, this.imageFormatGroup, - }) : super(const CameraValue.uninitialized()); + }) : super(CameraValue.uninitialized(cameraDescription)); /// The properties of the camera device controlled by this controller. - final CameraDescription description; + CameraDescription get description => value.description; /// The resolution this controller is targeting. /// @@ -202,7 +209,9 @@ class CameraController extends ValueNotifier { int get cameraId => _cameraId; /// Initializes the camera on the device. - Future initialize() async { + Future initialize() => _initializeWithDescription(description); + + Future _initializeWithDescription(CameraDescription description) async { final Completer initializeCompleter = Completer(); @@ -234,6 +243,7 @@ class CameraController extends ValueNotifier { value = value.copyWith( isInitialized: true, + description: description, previewSize: await initializeCompleter.future .then((CameraInitializedEvent event) => Size( event.previewWidth, @@ -274,6 +284,16 @@ class CameraController extends ValueNotifier { previewPauseOrientation: const Optional.absent()); } + /// Sets the description of the camera + Future setDescription(CameraDescription description) async { + if (value.isRecordingVideo) { + await CameraPlatform.instance.setDescriptionWhileRecording(description); + value = value.copyWith(description: description); + } else { + await _initializeWithDescription(description); + } + } + /// Captures an image and returns the file where it was saved. /// /// Throws a [CameraException] if the capture fails. diff --git a/packages/camera/camera_avfoundation/example/lib/main.dart b/packages/camera/camera_avfoundation/example/lib/main.dart index 4d98aed9a4c..3731325a49f 100644 --- a/packages/camera/camera_avfoundation/example/lib/main.dart +++ b/packages/camera/camera_avfoundation/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); } } @@ -603,10 +603,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, ), ), ); @@ -639,17 +636,15 @@ 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, kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium, diff --git a/packages/camera/camera_avfoundation/example/pubspec.yaml b/packages/camera/camera_avfoundation/example/pubspec.yaml index 7c85ba80719..436e7c5662e 100644 --- a/packages/camera/camera_avfoundation/example/pubspec.yaml +++ b/packages/camera/camera_avfoundation/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.4.0 flutter: sdk: flutter path_provider: ^2.0.0 @@ -32,3 +32,9 @@ dev_dependencies: flutter: uses-material-design: true + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + camera_avfoundation: + path: ../../../camera/camera_avfoundation diff --git a/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m index b85f68d1f95..f9b2a911b67 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m @@ -261,6 +261,8 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call [_camera pausePreviewWithResult:result]; } else if ([@"resumePreview" isEqualToString:call.method]) { [_camera resumePreviewWithResult:result]; + } else if ([@"setDescriptionWhileRecording" isEqualToString:call.method]) { + [_camera setDescriptionWhileRecording:(call.arguments[@"cameraName"]) result:result]; } else { [result sendNotImplemented]; } diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h index 85b8e2ae06f..df2a155855d 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h @@ -95,6 +95,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)applyFocusMode:(FLTFocusMode)focusMode onDevice:(AVCaptureDevice *)captureDevice; - (void)pausePreviewWithResult:(FLTThreadSafeFlutterResult *)result; - (void)resumePreviewWithResult:(FLTThreadSafeFlutterResult *)result; +- (void)setDescriptionWhileRecording:(NSString *)cameraName + result:(FLTThreadSafeFlutterResult *)result; - (void)setExposurePointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y; - (void)setFocusPointWithResult:(FLTThreadSafeFlutterResult *)result x:(double)x y:(double)y; - (void)setExposureOffsetWithResult:(FLTThreadSafeFlutterResult *)result offset:(double)offset; diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m index a7d6cd24be3..d5247e00382 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m @@ -43,7 +43,8 @@ @interface FLTCam () setDescriptionWhileRecording( + CameraDescription description) async { + await _channel.invokeMethod( + 'setDescriptionWhileRecording', + { + 'cameraName': description.name, + }, + ); + } + @override Widget buildPreview(int cameraId) { return Texture(textureId: cameraId); diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index b272a4c5c68..78c9156a7b7 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.11 +version: 0.9.12 environment: sdk: ">=2.14.0 <3.0.0" @@ -17,7 +17,7 @@ flutter: dartPluginClass: AVFoundationCamera dependencies: - camera_platform_interface: ^2.3.1 + camera_platform_interface: ^2.4.0 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 5d0b74cf0c0..e756f38ff12 100644 --- a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart +++ b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart @@ -701,6 +701,29 @@ void main() { ]); }); + test('Should set the description while recording', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'setDescriptionWhileRecording': null}, + ); + const CameraDescription camera2Description = CameraDescription( + name: 'Test2', + lensDirection: CameraLensDirection.front, + sensorOrientation: 0); + + // Act + await camera.setDescriptionWhileRecording(camera2Description); + + // Assert + expect(channel.log, [ + isMethodCall('setDescriptionWhileRecording', + arguments: { + 'cameraName': camera2Description.name, + }), + ]); + }); + test('Should set the flash mode', () async { // Arrange final MethodChannelMock channel = MethodChannelMock( From 0acc3cabb1b328f60a2fa9acc478dd31b7fffc1d Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Mon, 20 Mar 2023 10:59:56 -0600 Subject: [PATCH 02/14] removes all changes outside of federated root camera package for the final pr --- .../java/io/flutter/plugins/camera/Camera.java | 12 ------------ .../io/flutter/plugins/camera/VideoRenderer.java | 5 ----- .../java/io/flutter/plugins/camera/CameraTest.java | 5 ----- .../example/integration_test/camera_test.dart | 14 -------------- .../camera/camera_android/example/pubspec.yaml | 8 -------- .../camera/camera_android_camerax/pubspec.yaml | 1 - .../example/integration_test/camera_test.dart | 12 ------------ .../example/ios/Runner.xcodeproj/project.pbxproj | 4 ---- .../example/ios/Runner/Info.plist | 3 --- .../camera_avfoundation/example/pubspec.yaml | 6 ------ .../camera_avfoundation/ios/Classes/FLTCam.m | 8 -------- 11 files changed, 78 deletions(-) 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 9d29a120dfc..500407009c5 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 @@ -330,12 +330,8 @@ public void onOpened(@NonNull CameraDevice device) { cameraFeatures.getExposurePoint().checkIsSupported(), cameraFeatures.getFocusPoint().checkIsSupported()); -<<<<<<< HEAD - } catch (CameraAccessException | InterruptedException e) { -======= } catch (Exception e) { Log.i(TAG, "open | onOpened error: " + e.getMessage()); ->>>>>>> main dartMessenger.sendCameraErrorEvent(e.getMessage()); close(); } @@ -1104,12 +1100,7 @@ private void startPreviewWithVideoRendererStream() // get rotation for rendered video final PlatformChannel.DeviceOrientation lockedOrientation = -<<<<<<< HEAD - ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) - .getLockedCaptureOrientation(); -======= cameraFeatures.getSensorOrientation().getLockedCaptureOrientation(); ->>>>>>> main DeviceOrientationManager orientationManager = cameraFeatures.getSensorOrientation().getDeviceOrientationManager(); @@ -1318,8 +1309,6 @@ public void setDescriptionWhileRecording( return; } -<<<<<<< HEAD -======= // See VideoRenderer.java requires API 26 to switch camera while recording if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) { result.error( @@ -1329,7 +1318,6 @@ public void setDescriptionWhileRecording( return; } ->>>>>>> main stopAndReleaseCamera(); prepareVideoRenderer(); cameraProperties = properties; diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java index 9227859cd8f..62a70640961 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java @@ -358,12 +358,7 @@ public void draw(int viewportWidth, int viewportHeight, float[] texMatrix) { EGLExt.eglPresentationTimeANDROID(display, surface, uptimeMillis() * 1000000); if (!EGL14.eglSwapBuffers(display, surface)) { -<<<<<<< HEAD - throw new RuntimeException( - "eglSwapBuffers()" + GLUtils.getEGLErrorString(EGL14.eglGetError())); -======= Log.w(TAG, "eglSwapBuffers() " + GLUtils.getEGLErrorString(EGL14.eglGetError())); ->>>>>>> main } } } diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java index 9d3e6891bcc..9de33e3dc7a 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -614,10 +614,6 @@ public void setDescriptionWhileRecording() { final CameraProperties newCameraProperties = mock(CameraProperties.class); camera.setDescriptionWhileRecording(mockResult, newCameraProperties); -<<<<<<< HEAD - verify(mockResult, times(1)).success(null); - verify(mockResult, never()).error(any(), any(), any()); -======= if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) { verify(mockResult, times(1)) .error( @@ -628,7 +624,6 @@ public void setDescriptionWhileRecording() { verify(mockResult, times(1)).success(null); verify(mockResult, never()).error(any(), any(), any()); } ->>>>>>> main } @Test 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 66da49e63e2..8d663074df7 100644 --- a/packages/camera/camera_android/example/integration_test/camera_test.dart +++ b/packages/camera/camera_android/example/integration_test/camera_test.dart @@ -223,13 +223,6 @@ void main() { await controller.prepareForVideoRecording(); await controller.startVideoRecording(); -<<<<<<< HEAD - sleep(const Duration(milliseconds: 500)); - await controller.setDescription(cameras[1]); - sleep(const Duration(milliseconds: 500)); - - expect(controller.description, cameras[1]); -======= // SDK < 26 will throw a platform error when trying to switch and keep the same camera // we accept either outcome here, while the native unit tests check the outcome based on the current Android SDK @@ -252,7 +245,6 @@ void main() { // cameras switched expect(controller.description, cameras[1]); } ->>>>>>> main }); testWidgets('Set description', (WidgetTester tester) async { @@ -269,13 +261,7 @@ void main() { ); await controller.initialize(); -<<<<<<< HEAD - sleep(const Duration(milliseconds: 500)); - await controller.setDescription(cameras[1]); - sleep(const Duration(milliseconds: 500)); -======= await controller.setDescription(cameras[1]); ->>>>>>> main expect(controller.description, cameras[1]); }); diff --git a/packages/camera/camera_android/example/pubspec.yaml b/packages/camera/camera_android/example/pubspec.yaml index f3b343bde62..8218273cdec 100644 --- a/packages/camera/camera_android/example/pubspec.yaml +++ b/packages/camera/camera_android/example/pubspec.yaml @@ -33,11 +33,3 @@ dev_dependencies: flutter: uses-material-design: true -<<<<<<< HEAD - -# FOR TESTING ONLY. DO NOT MERGE. -dependency_overrides: - camera_android: - path: ../../../camera/camera_android -======= ->>>>>>> main diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 91a645510d9..5034d41fcfc 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -32,4 +32,3 @@ dev_dependencies: sdk: flutter mockito: 5.3.2 pigeon: ^3.2.6 - 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 6e18325e43c..315a34e9f9b 100644 --- a/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart +++ b/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart @@ -215,13 +215,7 @@ void main() { await controller.prepareForVideoRecording(); await controller.startVideoRecording(); -<<<<<<< HEAD - sleep(const Duration(milliseconds: 500)); - await controller.setDescription(cameras[1]); - sleep(const Duration(milliseconds: 500)); -======= await controller.setDescription(cameras[1]); ->>>>>>> main expect(controller.description, cameras[1]); }); @@ -240,13 +234,7 @@ void main() { ); await controller.initialize(); -<<<<<<< HEAD - sleep(const Duration(milliseconds: 500)); - await controller.setDescription(cameras[1]); - sleep(const Duration(milliseconds: 500)); -======= await controller.setDescription(cameras[1]); ->>>>>>> main expect(controller.description, cameras[1]); }); diff --git a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj index c2d0d362063..a20638d18e1 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,11 +3,7 @@ archiveVersion = 1; classes = { }; -<<<<<<< HEAD - objectVersion = 50; -======= objectVersion = 54; ->>>>>>> main objects = { /* Begin PBXBuildFile section */ diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist b/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist index ce706cda836..ca93baac701 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist +++ b/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist @@ -56,10 +56,7 @@ CADisableMinimumFrameDurationOnPhone -<<<<<<< HEAD -======= UIApplicationSupportsIndirectInputEvents ->>>>>>> main diff --git a/packages/camera/camera_avfoundation/example/pubspec.yaml b/packages/camera/camera_avfoundation/example/pubspec.yaml index bcc585af0c1..0c471ff4848 100644 --- a/packages/camera/camera_avfoundation/example/pubspec.yaml +++ b/packages/camera/camera_avfoundation/example/pubspec.yaml @@ -32,9 +32,3 @@ dev_dependencies: flutter: uses-material-design: true - - -# FOR TESTING ONLY. DO NOT MERGE. -dependency_overrides: - camera_avfoundation: - path: ../../../camera/camera_avfoundation diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m index c6399e23191..31bffc91794 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m @@ -155,18 +155,10 @@ - (instancetype)initWithCameraName:(NSString *)cameraName [_videoCaptureSession addOutputWithNoConnections:_captureVideoOutput]; [_videoCaptureSession addConnection:connection]; -<<<<<<< HEAD - if (@available(iOS 10.0, *)) { - _capturePhotoOutput = [AVCapturePhotoOutput new]; - [_capturePhotoOutput setHighResolutionCaptureEnabled:YES]; - [_videoCaptureSession addOutput:_capturePhotoOutput]; - } -======= _capturePhotoOutput = [AVCapturePhotoOutput new]; [_capturePhotoOutput setHighResolutionCaptureEnabled:YES]; [_videoCaptureSession addOutput:_capturePhotoOutput]; ->>>>>>> main _motionManager = [[CMMotionManager alloc] init]; [_motionManager startAccelerometerUpdates]; From f8735ca7452cb09f6be4d554c87a40f1610801b9 Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Mon, 20 Mar 2023 11:42:02 -0600 Subject: [PATCH 03/14] updates pubspec.yaml to point to correct camera_avfoundation and camera_android packages. Fixes a couple things from the merge I originally missed --- packages/camera/camera/CHANGELOG.md | 5 ----- packages/camera/camera/pubspec.yaml | 15 ++------------- packages/camera/camera_android/CHANGELOG.md | 3 --- packages/camera/camera_avfoundation/CHANGELOG.md | 6 ------ packages/camera/camera_avfoundation/pubspec.yaml | 4 ---- 5 files changed, 2 insertions(+), 31 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 05fb96460dc..7704b80ed62 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,10 +1,6 @@ -<<<<<<< HEAD ## 0.10.4 * Allows camera to be switched while video recording. -======= -## NEXT - * Aligns Dart and Flutter SDK constraints. ## 0.10.3+2 @@ -14,7 +10,6 @@ ## 0.10.3+1 * Updates links for the merge of flutter/plugins into flutter/packages. ->>>>>>> main ## 0.10.3 diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index eb2c6394729..fe063157956 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,11 +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 -<<<<<<< HEAD version: 0.10.4 -======= -version: 0.10.3+2 ->>>>>>> main environment: sdk: ">=2.17.0 <3.0.0" @@ -25,8 +21,8 @@ flutter: default_package: camera_web dependencies: - camera_android: ^0.10.1 - camera_avfoundation: ^0.9.9 + camera_android: ^0.10.5 + camera_avfoundation: ^0.9.13 camera_platform_interface: ^2.4.0 camera_web: ^0.3.1 flutter: @@ -43,10 +39,3 @@ dev_dependencies: plugin_platform_interface: ^2.0.0 video_player: ^2.0.0 - -# FOR TESTING ONLY. DO NOT MERGE. -dependency_overrides: - camera_android: - path: ../../camera/camera_android - camera_avfoundation: - path: ../../camera/camera_avfoundation diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md index c4f49ca0c6e..c4c772bfd60 100644 --- a/packages/camera/camera_android/CHANGELOG.md +++ b/packages/camera/camera_android/CHANGELOG.md @@ -1,8 +1,6 @@ ## 0.10.5 * Allows camera to be switched while video recording. -<<<<<<< HEAD -======= ## 0.10.4+3 * Clarifies explanation of endorsement in README. @@ -18,7 +16,6 @@ ## 0.10.4+1 * Updates links for the merge of flutter/plugins into flutter/packages. ->>>>>>> main ## 0.10.4 diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index f46a421d15a..a3e714fc33d 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,8 +1,3 @@ -<<<<<<< HEAD -## 0.9.12 - -* Allows camera to be switched while video recording. -======= ## 0.9.13+1 * Clarifies explanation of endorsement in README. @@ -19,7 +14,6 @@ ## 0.9.11+1 * Updates links for the merge of flutter/plugins into flutter/packages. ->>>>>>> main ## 0.9.11 diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index c44372e8607..e2c7dc4695f 100644 --- a/packages/camera/camera_avfoundation/pubspec.yaml +++ b/packages/camera/camera_avfoundation/pubspec.yaml @@ -2,11 +2,7 @@ name: camera_avfoundation description: iOS implementation of the camera plugin. repository: https://github.com/flutter/packages/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 -<<<<<<< HEAD -version: 0.9.12 -======= version: 0.9.13+1 ->>>>>>> main environment: sdk: '>=2.18.0 <3.0.0' From 297db559d83f87841648814ea7190e5da4ee5814 Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Mon, 20 Mar 2023 11:55:48 -0600 Subject: [PATCH 04/14] removes extra whitespace --- packages/camera/camera/pubspec.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index fe063157956..43b51c0479e 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -38,4 +38,3 @@ dev_dependencies: mockito: 5.3.2 plugin_platform_interface: ^2.0.0 video_player: ^2.0.0 - From 9072bd6ac6ed3af6244d7b6a9e3853426fffcaf1 Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Thu, 23 Mar 2023 09:28:56 -0600 Subject: [PATCH 05/14] dummy commit to retrigger tests --- packages/camera/camera/lib/src/camera_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 69917d3f039..68e24bf9c37 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -395,7 +395,7 @@ class CameraController extends ValueNotifier { } /// Sets the description of the camera. - /// + /// /// Throws a [CameraException] if setting the description fails. Future setDescription(CameraDescription description) async { if (value.isRecordingVideo) { From 37d53c3b3032d27937aea3a0f755b30ca99e71b5 Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Thu, 23 Mar 2023 09:29:12 -0600 Subject: [PATCH 06/14] removes space added for dummy commit --- packages/camera/camera/lib/src/camera_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 68e24bf9c37..69917d3f039 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -395,7 +395,7 @@ class CameraController extends ValueNotifier { } /// Sets the description of the camera. - /// + /// /// Throws a [CameraException] if setting the description fails. Future setDescription(CameraDescription description) async { if (value.isRecordingVideo) { From 70a3d382c8189e255a682759584034bc5196f4da Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Mon, 27 Mar 2023 11:19:51 -0600 Subject: [PATCH 07/14] dummy commit for tests to run --- packages/camera/camera/lib/src/camera_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 69917d3f039..ecf0a8b4bb1 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -394,7 +394,7 @@ class CameraController extends ValueNotifier { } } - /// Sets the description of the camera. + /// Sets the description of the camera. /// /// Throws a [CameraException] if setting the description fails. Future setDescription(CameraDescription description) async { From cbe60ec3ba550af2d9236202645b2490b77903d4 Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Mon, 27 Mar 2023 11:20:05 -0600 Subject: [PATCH 08/14] removes space in dummy commit --- packages/camera/camera/lib/src/camera_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index ecf0a8b4bb1..69917d3f039 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -394,7 +394,7 @@ class CameraController extends ValueNotifier { } } - /// Sets the description of the camera. + /// Sets the description of the camera. /// /// Throws a [CameraException] if setting the description fails. Future setDescription(CameraDescription description) async { From 0f687cef2dbaddddf00e24ba36607a8188df89af Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Tue, 28 Mar 2023 08:33:33 -0600 Subject: [PATCH 09/14] camera requires >= flutter 3.3.0 --- packages/camera/camera/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 43b51c0479e..06c212398e8 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -8,7 +8,7 @@ version: 0.10.4 environment: sdk: ">=2.17.0 <3.0.0" - flutter: ">=3.0.0" + flutter: ">=3.3.0" flutter: plugin: From 0cfb6bfb3071cace90258a6de66cd208bba90fcf Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Tue, 28 Mar 2023 08:33:59 -0600 Subject: [PATCH 10/14] adds comma to reformat --- .../camera/camera/test/camera_value_test.dart | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index 036b8d7f4a0..e23b865f30a 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -19,22 +19,23 @@ void main() { group('camera_value', () { test('Can be created', () { const CameraValue cameraValue = CameraValue( - isInitialized: false, - previewSize: Size(10, 10), - isRecordingPaused: false, - isRecordingVideo: false, - isTakingPicture: false, - isStreamingImages: false, - flashMode: FlashMode.auto, - exposureMode: ExposureMode.auto, - exposurePointSupported: true, - focusMode: FocusMode.auto, - deviceOrientation: DeviceOrientation.portraitUp, - lockedCaptureOrientation: DeviceOrientation.portraitUp, - recordingOrientation: DeviceOrientation.portraitUp, - focusPointSupported: true, - previewPauseOrientation: DeviceOrientation.portraitUp, - description: FakeController.fakeDescription); + isInitialized: false, + previewSize: Size(10, 10), + isRecordingPaused: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + exposurePointSupported: true, + focusMode: FocusMode.auto, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: DeviceOrientation.portraitUp, + recordingOrientation: DeviceOrientation.portraitUp, + focusPointSupported: true, + previewPauseOrientation: DeviceOrientation.portraitUp, + description: FakeController.fakeDescription, + ); expect(cameraValue, isA()); expect(cameraValue.isInitialized, isFalse); From a326d890a5daf70d773319bb3756d41cbd33e60a Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Tue, 28 Mar 2023 08:35:22 -0600 Subject: [PATCH 11/14] removes unneeded delays in tests --- .../camera/camera/example/integration_test/camera_test.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/camera/camera/example/integration_test/camera_test.dart b/packages/camera/camera/example/integration_test/camera_test.dart index 5338270828e..6bef30e62c2 100644 --- a/packages/camera/camera/example/integration_test/camera_test.dart +++ b/packages/camera/camera/example/integration_test/camera_test.dart @@ -281,9 +281,7 @@ void main() { await controller.prepareForVideoRecording(); await controller.startVideoRecording(); - sleep(const Duration(milliseconds: 500)); await controller.setDescription(cameras[1]); - sleep(const Duration(milliseconds: 500)); expect(controller.description, cameras[1]); }); @@ -301,9 +299,7 @@ void main() { ); await controller.initialize(); - sleep(const Duration(milliseconds: 500)); await controller.setDescription(cameras[1]); - sleep(const Duration(milliseconds: 500)); expect(controller.description, cameras[1]); }); From 961bae43d0e892f234cf444406935b40230a02b6 Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Tue, 28 Mar 2023 08:42:36 -0600 Subject: [PATCH 12/14] dart lower sdk version >= 2.18.0 --- packages/camera/camera/example/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml index 9841d0adfa6..3c4de9145ee 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -3,7 +3,7 @@ description: Demonstrates how to use the camera plugin. publish_to: none environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=2.18.0 <3.0.0" flutter: ">=3.0.0" dependencies: From b49cdacab9f3a590b50a1c5bae83be73f6fdb6a7 Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Tue, 28 Mar 2023 08:56:59 -0600 Subject: [PATCH 13/14] changes minimum constraints for camera/example and camera --- packages/camera/camera/example/pubspec.yaml | 2 +- packages/camera/camera/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml index 3c4de9145ee..5b74744dcc8 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.18.0 <3.0.0" - flutter: ">=3.0.0" + flutter: ">=3.3.0" dependencies: camera: diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 06c212398e8..69a7804f8b1 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -7,7 +7,7 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 0.10.4 environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=2.18.0 <3.0.0" flutter: ">=3.3.0" flutter: From 81dd1d86815a59a4cb4c87e294eb0861124c3747 Mon Sep 17 00:00:00 2001 From: BradenBagby Date: Mon, 3 Apr 2023 14:11:45 -0600 Subject: [PATCH 14/14] fixes version --- packages/camera/camera/CHANGELOG.md | 5 +---- packages/camera/camera/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 54403503ade..1a783b16391 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,9 +1,6 @@ -## 0.10.5 - -* Allows camera to be switched while video recording. - ## 0.10.4 +* Allows camera to be switched while video recording. * Updates minimum Flutter version to 3.3. * Aligns Dart and Flutter SDK constraints. diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 739649b092a..839f064bba1 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.10.5 +version: 0.10.4 environment: sdk: ">=2.18.0 <4.0.0"