From 95853567e9ade7ebe27a2af17bb82c90f4cf8416 Mon Sep 17 00:00:00 2001 From: otsmr Date: Wed, 7 Oct 2026 22:18:23 +0200 Subject: [PATCH] add new dep --- .../lib/src/android_camera_camerax.dart | 15 +- camera_avfoundation/AUTHORS | 67 + camera_avfoundation/CHANGELOG.md | 446 +++++ camera_avfoundation/LICENSE | 25 + camera_avfoundation/README.md | 15 + camera_avfoundation/TWONLY_CHANGES.md | 11 + .../darwin/Tests/AvailableCamerasTests.swift | 138 ++ .../darwin/Tests/CameraExposureTests.swift | 146 ++ .../Tests/CameraImageStreamScalerTests.swift | 43 + .../Tests/CameraInitRaceConditionsTests.swift | 92 + .../Tests/CameraMethodChannelTests.swift | 80 + .../darwin/Tests/CameraOrientationTests.swift | 168 ++ .../darwin/Tests/CameraPermissionTests.swift | 237 +++ .../Tests/CameraPluginCreateCameraTests.swift | 131 ++ .../CameraPluginDelegatingMethodTests.swift | 652 ++++++ .../CameraPluginInitializeCameraTests.swift | 105 + .../Tests/CameraPreviewPauseTests.swift | 26 + .../darwin/Tests/CameraPropertiesTests.swift | 73 + .../Tests/CameraSessionPresetsTests.swift | 97 + .../CameraSetDeviceOrientationTests.swift | 108 + .../Tests/CameraSetFlashModeTests.swift | 168 ++ .../Tests/CameraSetFocusModeTests.swift | 166 ++ .../darwin/Tests/CameraSettingsTests.swift | 356 ++++ .../darwin/Tests/CameraTestUtils.swift | 219 +++ .../darwin/Tests/CameraZoomTests.swift | 111 ++ .../darwin/Tests/Mocks/MockAssetWriter.swift | 33 + .../Tests/Mocks/MockAssetWriterInput.swift | 25 + ...ckAssetWriterInputPixelBufferAdaptor.swift | 17 + .../darwin/Tests/Mocks/MockCamera.swift | 238 +++ .../Mocks/MockCameraDeviceDiscoverer.swift | 29 + .../Tests/Mocks/MockCaptureConnection.swift | 27 + .../Tests/Mocks/MockCaptureDevice.swift | 127 ++ .../Tests/Mocks/MockCaptureDeviceFormat.swift | 43 + .../Mocks/MockCaptureDeviceInputFactory.swift | 15 + .../darwin/Tests/Mocks/MockCaptureInput.swift | 17 + .../Tests/Mocks/MockCapturePhotoOutput.swift | 32 + .../Tests/Mocks/MockCaptureSession.swift | 87 + .../Mocks/MockCaptureVideoDataOutput.swift | 27 + .../Mocks/MockDeviceOrientationProvider.swift | 15 + .../MockFLTCameraPermissionManager.swift | 32 + .../Mocks/MockFlutterBinaryMessenger.swift | 26 + .../Mocks/MockFlutterTextureRegistry.swift | 16 + .../Tests/Mocks/MockFrameRateRange.swift | 19 + .../Tests/Mocks/MockGlobalEventApi.swift | 26 + .../darwin/Tests/Mocks/MockWritableData.swift | 19 + .../darwin/Tests/PhotoCaptureTests.swift | 248 +++ .../darwin/Tests/QueueUtilsTests.swift | 35 + .../darwin/Tests/SampleBufferTests.swift | 542 +++++ .../darwin/Tests/SavePhotoDelegateTests.swift | 99 + .../darwin/Tests/StreamingTests.swift | 217 ++ .../darwin/camera_avfoundation.podspec | 27 + .../darwin/camera_avfoundation/Package.swift | 28 + .../camera_avfoundation/AssetWriter.swift | 45 + .../Sources/camera_avfoundation/Camera.swift | 133 ++ .../CameraConfiguration.swift | 78 + .../CameraDeviceDiscoverer.swift | 31 + .../CameraImageStreamScaler.swift | 41 + .../CameraPermissionManager.swift | 136 ++ .../camera_avfoundation/CameraPlugin.swift | 567 ++++++ .../CameraProperties.swift | 102 + .../CaptureConnection.swift | 30 + .../camera_avfoundation/CaptureDevice.swift | 118 ++ .../CaptureDeviceFormat.swift | 31 + .../camera_avfoundation/CaptureOutput.swift | 77 + .../camera_avfoundation/CaptureSession.swift | 49 + .../camera_avfoundation/DefaultCamera.swift | 1544 +++++++++++++++ .../DeviceOrientationProvider.swift | 20 + .../camera_avfoundation/FormatUtils.swift | 75 + .../ImageStreamHandler.swift | 44 + .../MediaSettingsAVWrapper.swift | 99 + .../camera_avfoundation/Messages.swift | 1747 +++++++++++++++++ .../PermissionServicing.swift | 29 + .../camera_avfoundation/QueueUtils.swift | 25 + .../Resources/PrivacyInfo.xcprivacy | 14 + .../SavePhotoDelegate.swift | 85 + .../camera_avfoundation/WritableData.swift | 20 + .../lib/camera_avfoundation.dart | 5 + .../lib/src/avfoundation_camera.dart | 650 ++++++ camera_avfoundation/lib/src/messages.g.dart | 1400 +++++++++++++ .../lib/src/type_conversion.dart | 50 + camera_avfoundation/lib/src/utils.dart | 84 + camera_avfoundation/pigeons/copyright.txt | 3 + camera_avfoundation/pigeons/messages.dart | 375 ++++ camera_avfoundation/pubspec.yaml | 37 + .../test/avfoundation_camera_test.dart | 798 ++++++++ .../test/avfoundation_camera_test.mocks.dart | 354 ++++ .../test/flutter_test_config.dart | 13 + .../test/type_conversion_test.dart | 59 + camera_avfoundation/test/utils_test.dart | 66 + 89 files changed, 14794 insertions(+), 1 deletion(-) create mode 100644 camera_avfoundation/AUTHORS create mode 100644 camera_avfoundation/CHANGELOG.md create mode 100644 camera_avfoundation/LICENSE create mode 100644 camera_avfoundation/README.md create mode 100644 camera_avfoundation/TWONLY_CHANGES.md create mode 100644 camera_avfoundation/darwin/Tests/AvailableCamerasTests.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraExposureTests.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraImageStreamScalerTests.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraInitRaceConditionsTests.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraMethodChannelTests.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraOrientationTests.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraPermissionTests.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraPluginCreateCameraTests.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraPluginDelegatingMethodTests.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraPluginInitializeCameraTests.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraPreviewPauseTests.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraPropertiesTests.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraSessionPresetsTests.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraSetDeviceOrientationTests.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraSetFlashModeTests.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraSetFocusModeTests.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraSettingsTests.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraTestUtils.swift create mode 100644 camera_avfoundation/darwin/Tests/CameraZoomTests.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockAssetWriter.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockAssetWriterInput.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockAssetWriterInputPixelBufferAdaptor.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockCamera.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockCameraDeviceDiscoverer.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockCaptureConnection.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockCaptureDevice.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockCaptureDeviceFormat.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockCaptureDeviceInputFactory.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockCaptureInput.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockCapturePhotoOutput.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockCaptureSession.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockCaptureVideoDataOutput.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockDeviceOrientationProvider.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockFLTCameraPermissionManager.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockFlutterBinaryMessenger.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockFlutterTextureRegistry.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockFrameRateRange.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockGlobalEventApi.swift create mode 100644 camera_avfoundation/darwin/Tests/Mocks/MockWritableData.swift create mode 100644 camera_avfoundation/darwin/Tests/PhotoCaptureTests.swift create mode 100644 camera_avfoundation/darwin/Tests/QueueUtilsTests.swift create mode 100644 camera_avfoundation/darwin/Tests/SampleBufferTests.swift create mode 100644 camera_avfoundation/darwin/Tests/SavePhotoDelegateTests.swift create mode 100644 camera_avfoundation/darwin/Tests/StreamingTests.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation.podspec create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Package.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/AssetWriter.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/Camera.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraConfiguration.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraDeviceDiscoverer.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraImageStreamScaler.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraPermissionManager.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraProperties.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureConnection.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureDevice.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureDeviceFormat.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureOutput.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureSession.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/DeviceOrientationProvider.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/FormatUtils.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/ImageStreamHandler.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/MediaSettingsAVWrapper.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/Messages.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/PermissionServicing.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/QueueUtils.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/Resources/PrivacyInfo.xcprivacy create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/SavePhotoDelegate.swift create mode 100644 camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/WritableData.swift create mode 100644 camera_avfoundation/lib/camera_avfoundation.dart create mode 100644 camera_avfoundation/lib/src/avfoundation_camera.dart create mode 100644 camera_avfoundation/lib/src/messages.g.dart create mode 100644 camera_avfoundation/lib/src/type_conversion.dart create mode 100644 camera_avfoundation/lib/src/utils.dart create mode 100644 camera_avfoundation/pigeons/copyright.txt create mode 100644 camera_avfoundation/pigeons/messages.dart create mode 100644 camera_avfoundation/pubspec.yaml create mode 100644 camera_avfoundation/test/avfoundation_camera_test.dart create mode 100644 camera_avfoundation/test/avfoundation_camera_test.mocks.dart create mode 100644 camera_avfoundation/test/flutter_test_config.dart create mode 100644 camera_avfoundation/test/type_conversion_test.dart create mode 100644 camera_avfoundation/test/utils_test.dart diff --git a/camera_android_camerax/lib/src/android_camera_camerax.dart b/camera_android_camerax/lib/src/android_camera_camerax.dart index 0269832..2b80f4d 100644 --- a/camera_android_camerax/lib/src/android_camera_camerax.dart +++ b/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -466,7 +466,20 @@ class AndroidCameraCameraX extends CameraPlatform { imageFormatGroup, ); imageAnalysis = ImageAnalysis( - resolutionSelector: _presetResolutionSelector, + // QR analysis should never copy full-HD frames through the Dart bridge. + // Selfie filters use the already reduced 720p preview resolution. + resolutionSelector: cameraIsFrontFacing + ? _presetResolutionSelector + : ResolutionSelector( + resolutionStrategy: ResolutionStrategy( + boundSize: CameraSize(width: 640, height: 360), + fallbackRule: ResolutionStrategyFallbackRule.closestLowerThenHigher, + ), + aspectRatioStrategy: AspectRatioStrategy( + preferredAspectRatio: AspectRatio.ratio16To9, + fallbackRule: AspectRatioStrategyFallbackRule.auto, + ), + ), targetFpsRange: _targetFpsRange, outputImageFormat: _imageAnalysisOutputImageFormat, ); diff --git a/camera_avfoundation/AUTHORS b/camera_avfoundation/AUTHORS new file mode 100644 index 0000000..605414a --- /dev/null +++ b/camera_avfoundation/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Rui Craveiro diff --git a/camera_avfoundation/CHANGELOG.md b/camera_avfoundation/CHANGELOG.md new file mode 100644 index 0000000..b69f57f --- /dev/null +++ b/camera_avfoundation/CHANGELOG.md @@ -0,0 +1,446 @@ +## 0.10.3 + +* Updates pigeon dev_dependency to ^27.3.2 for analyzer 14 compatibility. + +## 0.10.2 + +* Adds `setJpegImageQuality` for controlling JPEG compression quality. +* Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. + +## 0.10.1 + +* Fixes fatal crash on iPhone 17 when using `ResolutionPreset.max`. + +## 0.10.0+3 + +* Switches to type-safe Pigeon code for image streaming. + +## 0.10.0+2 + +* Updates example to demonstrate correct exception handling for async return statements, ensuring exceptions thrown during return within try blocks are properly caught as per [dart-lang/sdk#44395](https://github.com/dart-lang/sdk/issues/44395). + +## 0.10.0+1 + +* Migrates to Swift-based pigeon interface. + +## 0.10.0 + +* Adds video stabilization. + - Adds getSupportedVideoStabilizationModes method. + - Adds setVideoStabilizationMode method. + +## 0.9.23+2 + +* Code refactor related to Swift pigeon's generated struct MediaSettings being immutable. + +## 0.9.23+1 + +* Fixes error handling in `startWriting`. + +## 0.9.23 + +* Changes the default storage path for camera pictures and videos on iOS from the application's Documents directory to the system's temporaryDirectory. + +## 0.9.22+10 + +* Handles video and audio interruptions and errors by "pausing" and reporting errors. +* Uses a single time offset for both video and audio to avoid desync. + +## 0.9.22+9 + +* Migrates `FLTSavePhotoDelegate`, `FLTWritableData`, and `FLTImageStreamHandler` classes to Swift. +* Migrates `CameraProperties` and `QueueUtils` helpers to Swift. + +## 0.9.22+8 + +* Migrates `FLTPermissionServicing` and `FLTCameraPermissionManager` classes to Swift. + +## 0.9.22+7 + +* Migrates `FLTCaptureConnection`, `FLTCaptureDeviceFormat` and `FLTAssetWriter` classes to Swift. + +## 0.9.22+6 + +* Updates to Pigeon 26. + +## 0.9.22+5 + +* Migrates `FLTCaptureDevice`, `FLTCaptureSession`, and `FLTFormatUtils` classes to Swift. + +## 0.9.22+4 + +* Migrates `FLTCameraDeviceDiscovering` and `FLTDeviceOrientationProviding` classes to Swift. + +## 0.9.22+3 + +* Updates examples to use the new RadioGroup API instead of deprecated Radio parameters. + +## 0.9.22+2 + +* Migrates `FLTCamConfiguration`, `FLTCamMediaSettingsAVWrapper` classes to Swift. +* Migrates `FLTCaptureOutput`, `FLTCapturePhotoOutput`, `FLTCaptureVideoDataOutput` protocols to Swift. + +## 0.9.22+1 + +* Fixes crash on iOS when `enableAudio` is false. + +## 0.9.22 + +* Adds lensType in the PlatformCameraDescription + +## 0.9.21+4 + +* Migrates `updateOrientation` and `setCaptureSessionPreset` methods to Swift. +* Removes `FLTCam` class. + +## 0.9.21+3 + +* Removes code for versions of iOS older than 13.0. +* Updates minimum supported SDK version to Flutter 3.35. + +## 0.9.21+2 + +* Migrates `DefaultCamera` initialization to Swift. +* Removes unused `textureId` field of `FLTCam` class. +* Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. + +## 0.9.21+1 + +* Migrates `startImageStream` and `setUpCaptureSessionForAudioIfNeeded` methods to Swift. +* Removes Objective-C implementation of `reportErrorMessage` method. + +## 0.9.21 + +* Fixes crash when streaming is enabled during recording. + +## 0.9.20+7 + +* Updates kotlin version to 2.2.0 to enable gradle 8.11 support. + +## 0.9.20+6 + +* Migrates `captureToFile` and `getTemporaryFilePath` methods to Swift. +* Switches to Swift dispatch queue specific interface. + +## 0.9.20+5 + +* Migrates `startVideoRecording`, `setUpVideoRecording`, and `setupWriter` methods to Swift. + +## 0.9.20+4 + +* Migrates `setVideoFormat`,`stopVideoRecording`, and `stopImageStream` methods to Swift. +* Migrates stopping accelerometer updates to Swift. +* Migrates `setDescriptionWhileRecording` method to Swift. +* Adds `createConnection` method implementation to Swift. + +## 0.9.20+3 + +* Migrates `setZoomLevel` and `setFlashMode` methods to Swift. + +## 0.9.20+2 + +* Migrates exposure offset and zoom factor limit getters to Swift. +* Migrates `setImageFileFormat` method to Swift. +* Migrates pause and resume methods to Swift. +* Migrates capture orientation locking methods to Swift. +* Converts `setDeviceOrientation` method to property setter and migrated to Swift. + +## 0.9.20+1 + +* Migrates lifecycle methods (`start`, `stop`, `close`) to Swift. +* Migrates exposure and focus related methods to Swift. +* Migrates `receivedImageStreamData` and `reportInitializationState` methods to Swift. + +## 0.9.20 + +* Fixes incorrect types in image stream events. + +## 0.9.19+3 + +* Fixes race condition when starting image stream. + +## 0.9.19+2 + +* Adds the `Camera` Swift protocol. +* Adds `DefaultCamera`, a `FLTCam`-based implementation of the `Camera` protocol. +* Migrates sample buffer delegates and `FlutterTexture` protocol implementations to `DefaultCamera`. + +## 0.9.19+1 + +* Adds `audioCaptureDeviceFactory` to `FLTCamConfiguration`. +* Renames the `lockCaptureOrientation` method of Objective-C type `FLTCam` when exported to Swift. +* Renames arguments of the `captureOutput` method of Objective-C type `FLTCam` when exported to Swift. +* Changes the `connection` argument type of the `captureOutput` method of the of `FLTCam` class to `AVCaptureConnection`. +* Makes `minimum/maximumAvailableZoomFactor` and `minimum/maximumExposureOffset` fields of `FLTCam` readonly. +* Updates minimum supported SDK version to Flutter 3.27/Dart 3.6. + +## 0.9.19 + +* Migrates the CameraPlugin class to Swift. +* Fixes camera name being ignored in `setDescriptionWhileRecording`. + +## 0.9.18+14 + +* Creates Swift Package Manager target for Swift implementation. + +## 0.9.18+13 + +* Migrates test utils and mocks to Swift. +* Aligns `FLTCaptureDevice` more closely with the `AVCaptureDevice` interface. + +## 0.9.18+12 + +* Migrates test utils and mocks to Swift. +* Renames the methods of Objective-C type `FLTCaptureSession` when exported to Swift. + +## 0.9.18+11 + +* Backfills unit tests for the `FLTCam` class. +* Refactors implementation to allow mocking of `AVCaptureVideoDataOutput` in tests. + +## 0.9.18+10 + +* Backfills unit tests for the `FLTCam` class. +* Makes `exposureMode`, `focusMode`, and `flashMode` properties of `FLTCam` private. + +## 0.9.18+9 + +* Backfills unit tests for `CameraPlugin` class. +* Adds `minimumExposureOffset` and `maximumExposureOffset` methods to `FLTCam` class. + +## 0.9.18+8 + +* Migrates unit tests to Swift. + +## 0.9.18+7 + +* Fixes crash when setting `activeFormat` on `FLTCaptureDevice`. + +## 0.9.18+6 + +* Refactors implementations to remove remaining usage of OCMock in internal testing. + +## 0.9.18+5 + +* Migrates unit tests to Swift. + +## 0.9.18+4 + +* Refactors implementations to reduce usage of OCMock in internal testing. + +## 0.9.18+3 + +* Refactors implementations to reduce usage of OCMock in internal testing. + +## 0.9.18+2 + +* Refactors implementations to reduce usage of OCMock in internal testing. + +## 0.9.18+1 + +* Refactors implementations to reduce usage of OCMock in internal testing. + +## 0.9.18 + +* Adds API support query for image streaming. + +## 0.9.17+7 + +* Fixes changing global audio session category to be collision free across plugins. + +## 0.9.17+6 + +* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. +* Removes OCMock usage from permission tests + +## 0.9.17+5 + +* Adds ability to use any supported FPS and fixes crash when using unsupported FPS. + +## 0.9.17+4 + +* Updates Pigeon for non-nullable collection type support. +* Updates minimum supported SDK version to Flutter 3.19/Dart 3.3. + +## 0.9.17+3 + +* Fixes deallocation of camera on dispose. + +## 0.9.17+2 + +* Fixes stopVideoRecording waiting indefinitely and lag at start of video. + +## 0.9.17+1 + +* Fixes a crash due to appending sample buffers when readyForMoreMediaData is NO. + +## 0.9.17 + +* Adds Swift Package Manager compatibility. + +## 0.9.16+3 + +* Removes unused `maxVideoDuration` code. + +## 0.9.16+2 + +* Fixes regression taking a picture in torch mode. + +## 0.9.16+1 + +* Fixes sample times not being numeric after pause/resume. + +## 0.9.16 + +* Converts Dart-to-host communcation to Pigeon. +* Fixes a race condition in camera disposal. + +## 0.9.15+4 + +* Converts host-to-Dart communcation to Pigeon. + +## 0.9.15+3 + +* Moves `pigeon` to `dev_dependencies`. + +## 0.9.15+2 + +* Converts camera query to Pigeon. + +## 0.9.15+1 + +* Simplifies internal handling of method channel responses. + +## 0.9.15 + +* Adds support to control video FPS and bitrate. See `CameraController.withSettings`. + +## 0.9.14+2 + +* Removes `_ambiguate` methods from example code. + +## 0.9.14+1 + +* Fixes bug where max resolution preset does not produce highest available resolution on iOS. + +## 0.9.14 + +* Adds support to HEIF format. + +## 0.9.13+11 + +* Fixes a memory leak of sample buffer when pause and resume the video recording. +* Removes development team from example app. +* Updates minimum iOS version to 12.0 and minimum Flutter version to 3.16.6. + +## 0.9.13+10 + +* Adds privacy manifest. + +## 0.9.13+9 + +* Fixes new lint warnings. + +## 0.9.13+8 + +* Updates example app to use non-deprecated video_player method. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. + +## 0.9.13+7 + +* Fixes inverted orientation strings. + +## 0.9.13+6 + +* Fixes incorrect use of `NSError` that could cause crashes on launch. + +## 0.9.13+5 + +* Ignores audio samples until the first video sample arrives. + +## 0.9.13+4 + +* Adds pub topics to package metadata. +* Updates minimum supported SDK version to Flutter 3.7/Dart 2.19. + +## 0.9.13+3 + +* Migrates `styleFrom` usage in examples off of deprecated `primary` and `onPrimary` parameters. +* Fixes unawaited_futures violations. + +## 0.9.13+2 + +* Removes obsolete null checks on non-nullable values. +* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + +## 0.9.13+1 + +* Clarifies explanation of endorsement in README. + +## 0.9.13 + +* Allows camera to be switched while video recording. +* Aligns Dart and Flutter SDK constraints. + +## 0.9.12 + +* Updates minimum Flutter version to 3.3 and iOS 11. + +## 0.9.11+1 + +* Updates links for the merge of flutter/plugins into flutter/packages. + +## 0.9.11 + +* Adds back use of Optional type. +* Updates minimum Flutter version to 3.0. + +## 0.9.10+2 + +* Updates code for stricter lint checks. + +## 0.9.10+1 + +* Updates code for stricter lint checks. + +## 0.9.10 + +* Remove usage of deprecated quiver Optional type. + +## 0.9.9 + +* Implements option to also stream when recording a video. + +## 0.9.8+6 + +* Updates code for `no_leading_underscores_for_local_identifiers` lint. +* Updates minimum Flutter version to 2.10. + +## 0.9.8+5 + +* Fixes a regression introduced in 0.9.8+4 where the stream handler is not set. + +## 0.9.8+4 + +* Fixes a crash due to sending orientation change events when the engine is torn down. + +## 0.9.8+3 + +* Fixes avoid_redundant_argument_values lint warnings and minor typos. +* Ignores missing return warnings in preparation for [upcoming analysis changes](https://github.com/flutter/flutter/issues/105750). + +## 0.9.8+2 + +* Fixes exception in registerWith caused by the switch to an in-package method channel. + +## 0.9.8+1 + +* Ignores deprecation warnings for upcoming styleFrom button API changes. + +## 0.9.8 + +* Switches to internal method channel implementation. + +## 0.9.7+1 + +* Splits from `camera` as a federated implementation. diff --git a/camera_avfoundation/LICENSE b/camera_avfoundation/LICENSE new file mode 100644 index 0000000..29b709d --- /dev/null +++ b/camera_avfoundation/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/camera_avfoundation/README.md b/camera_avfoundation/README.md new file mode 100644 index 0000000..459211b --- /dev/null +++ b/camera_avfoundation/README.md @@ -0,0 +1,15 @@ +# camera\_avfoundation + +The iOS implementation of [`camera`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `camera` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +[1]: https://pub.dev/packages/camera +[2]: https://flutter.dev/to/endorsed-federated-plugin diff --git a/camera_avfoundation/TWONLY_CHANGES.md b/camera_avfoundation/TWONLY_CHANGES.md new file mode 100644 index 0000000..12538de --- /dev/null +++ b/camera_avfoundation/TWONLY_CHANGES.md @@ -0,0 +1,11 @@ +Based on Flutter's `camera_avfoundation` 0.10.3 (BSD license). + +Local changes: + +- Downscale rear-camera BGRA analysis frames to a maximum edge of 640 pixels + with Accelerate, before copying them to Dart. Limit delivery to 10 fps. + Preview textures, video recording, and front-camera frame delivery are unchanged. +- Cover stream resizing with `CameraImageStreamScalerTests`. + +The app configures front-camera sessions at 720p and rear-camera sessions at +1080p. The shutter uses the preview screenshot path. diff --git a/camera_avfoundation/darwin/Tests/AvailableCamerasTests.swift b/camera_avfoundation/darwin/Tests/AvailableCamerasTests.swift new file mode 100644 index 0000000..4fbece5 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/AvailableCamerasTests.swift @@ -0,0 +1,138 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import XCTest + +@testable import camera_avfoundation + +final class AvailableCamerasTest: XCTestCase { + private func createCameraPlugin(with deviceDiscoverer: MockCameraDeviceDiscoverer) -> CameraPlugin + { + return CameraPlugin( + registry: MockFlutterTextureRegistry(), + messenger: MockFlutterBinaryMessenger(), + globalAPI: MockGlobalEventApi(), + deviceDiscoverer: deviceDiscoverer, + permissionManager: MockCameraPermissionManager(), + deviceFactory: { _ in MockCaptureDevice() }, + captureSessionFactory: { MockCaptureSession() }, + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue") + ) + } + + func testAvailableCamerasShouldReturnAllCamerasOnMultiCameraIPhone() { + let mockDeviceDiscoverer = MockCameraDeviceDiscoverer() + let cameraPlugin = createCameraPlugin(with: mockDeviceDiscoverer) + let expectation = self.expectation(description: "Result finished") + + mockDeviceDiscoverer.discoverySessionStub = { deviceTypes, mediaType, position in + // iPhone 13 Cameras: + let wideAngleCamera = MockCaptureDevice() + wideAngleCamera.uniqueID = "0" + wideAngleCamera.position = .back + + let frontFacingCamera = MockCaptureDevice() + frontFacingCamera.uniqueID = "1" + frontFacingCamera.position = .front + + let ultraWideCamera = MockCaptureDevice() + ultraWideCamera.uniqueID = "2" + ultraWideCamera.position = .back + + let telephotoCamera = MockCaptureDevice() + telephotoCamera.uniqueID = "3" + telephotoCamera.position = .back + + var requiredTypes: [AVCaptureDevice.DeviceType] = [ + .builtInWideAngleCamera, .builtInTelephotoCamera, .builtInUltraWideCamera, + ] + var cameras = [wideAngleCamera, frontFacingCamera, telephotoCamera, ultraWideCamera] + + XCTAssertEqual(deviceTypes, requiredTypes) + XCTAssertEqual(mediaType, .video) + XCTAssertEqual(position, .unspecified) + return cameras + } + + var resultValue: [PlatformCameraDescription]? + cameraPlugin.getAvailableCameras { result in + resultValue = self.assertSuccess(result) + expectation.fulfill() + } + waitForExpectations(timeout: 30, handler: nil) + + // Verify the result. + XCTAssertEqual(resultValue?.count, 4) + } + + func testAvailableCamerasShouldReturnTwoCamerasOnDualCameraIPhone() { + let mockDeviceDiscoverer = MockCameraDeviceDiscoverer() + let cameraPlugin = createCameraPlugin(with: mockDeviceDiscoverer) + let expectation = self.expectation(description: "Result finished") + + mockDeviceDiscoverer.discoverySessionStub = { deviceTypes, mediaType, position in + // iPhone 8 Cameras: + let wideAngleCamera = MockCaptureDevice() + wideAngleCamera.uniqueID = "0" + wideAngleCamera.position = .back + + let frontFacingCamera = MockCaptureDevice() + frontFacingCamera.uniqueID = "1" + frontFacingCamera.position = .front + + var requiredTypes: [AVCaptureDevice.DeviceType] = [ + .builtInWideAngleCamera, .builtInTelephotoCamera, .builtInUltraWideCamera, + ] + let cameras = [wideAngleCamera, frontFacingCamera] + + XCTAssertEqual(deviceTypes, requiredTypes) + XCTAssertEqual(mediaType, .video) + XCTAssertEqual(position, .unspecified) + return cameras + } + + var resultValue: [PlatformCameraDescription]? + cameraPlugin.getAvailableCameras { result in + resultValue = self.assertSuccess(result) + expectation.fulfill() + } + waitForExpectations(timeout: 30, handler: nil) + + // Verify the result. + XCTAssertEqual(resultValue?.count, 2) + } + + func testAvailableCamerasShouldReturnExternalLensDirectionForUnspecifiedCameraPosition() { + let mockDeviceDiscoverer = MockCameraDeviceDiscoverer() + let cameraPlugin = createCameraPlugin(with: mockDeviceDiscoverer) + let expectation = self.expectation(description: "Result finished") + + mockDeviceDiscoverer.discoverySessionStub = { deviceTypes, mediaType, position in + let unspecifiedCamera = MockCaptureDevice() + unspecifiedCamera.uniqueID = "0" + unspecifiedCamera.position = .unspecified + + var requiredTypes: [AVCaptureDevice.DeviceType] = [ + .builtInWideAngleCamera, .builtInTelephotoCamera, .builtInUltraWideCamera, + ] + let cameras = [unspecifiedCamera] + + XCTAssertEqual(deviceTypes, requiredTypes) + XCTAssertEqual(mediaType, .video) + XCTAssertEqual(position, .unspecified) + return cameras + } + + var resultValue: [PlatformCameraDescription]? + cameraPlugin.getAvailableCameras { result in + resultValue = self.assertSuccess(result) + expectation.fulfill() + } + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertEqual(resultValue?.first?.lensDirection, .external) + } +} diff --git a/camera_avfoundation/darwin/Tests/CameraExposureTests.swift b/camera_avfoundation/darwin/Tests/CameraExposureTests.swift new file mode 100644 index 0000000..da5531b --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraExposureTests.swift @@ -0,0 +1,146 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import XCTest + +@testable import camera_avfoundation + +final class CameraExposureTests: XCTestCase { + private func createCamera() -> (Camera, MockCaptureDevice, MockDeviceOrientationProvider) { + let mockDevice = MockCaptureDevice() + let mockDeviceOrientationProvider = MockDeviceOrientationProvider() + + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.videoCaptureDeviceFactory = { _ in mockDevice } + configuration.deviceOrientationProvider = mockDeviceOrientationProvider + let camera = CameraTestUtils.createTestCamera(configuration) + + return (camera, mockDevice, mockDeviceOrientationProvider) + } + + func testSetExposureModeLocked_setsAuthExposeMode() { + let (camera, mockDevice, _) = createCamera() + + mockDevice.setExposureModeStub = { mode in + // AVCaptureExposureModeAutoExpose automatically adjusts the exposure one time, and then + // locks exposure for the device + XCTAssertEqual(mode, .autoExpose) + } + + camera.setExposureMode(.locked) + } + + func testSetExposureModeAuto_setsContinousAutoExposureMode_ifSupported() { + let (camera, mockDevice, _) = createCamera() + + // All exposure modes are supported + mockDevice.isExposureModeSupportedStub = { _ in true } + + mockDevice.setExposureModeStub = { mode in + XCTAssertEqual(mode, .continuousAutoExposure) + } + + camera.setExposureMode(.auto) + } + + func testSetExposureModeAuto_setsAutoExposeMode_ifContinousAutoIsNotSupported() { + let (camera, mockDevice, _) = createCamera() + + // Continous auto exposure is not supported + mockDevice.isExposureModeSupportedStub = { mode in + mode != .continuousAutoExposure + } + + mockDevice.setExposureModeStub = { mode in + XCTAssertEqual(mode, .autoExpose) + } + + camera.setExposureMode(.auto) + } + + func testSetExposurePoint_setsExposurePointOfInterest() { + let (camera, mockDevice, mockDeviceOrientationProvider) = createCamera() + // UI is currently in landscape left orientation. + mockDeviceOrientationProvider.orientationStub = { .landscapeLeft } + // Exposure point of interest is supported. + mockDevice.isExposurePointOfInterestSupported = true + + // Verify the focus point of interest has been set. + var setPoint = CGPoint.zero + mockDevice.setExposurePointOfInterestStub = { point in + if point == CGPoint(x: 1, y: 1) { + setPoint = point + } + } + + let expectation = expectation(description: "Completion called") + camera.setExposurePoint(PlatformPoint(x: 1, y: 1)) { + result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + XCTAssertEqual(setPoint, CGPoint(x: 1.0, y: 1.0)) + } + + func testSetExposurePoint_returnsError_ifNotSupported() { + let (camera, mockDevice, mockDeviceOrientationProvider) = createCamera() + // UI is currently in landscape left orientation. + mockDeviceOrientationProvider.orientationStub = { .landscapeLeft } + // Exposure point of interest is not supported. + mockDevice.isExposurePointOfInterestSupported = false + + let expectation = expectation(description: "Completion with error") + + camera.setExposurePoint(PlatformPoint(x: 1, y: 1)) { result in + switch result { + case .failure(let error as PigeonError): + XCTAssertEqual(error.code, "setExposurePointFailed") + XCTAssertEqual(error.message, "Device does not have exposure point capabilities") + default: + XCTFail("Expected failure") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testSetExposureOffset_setsExposureTargetBias() { + let (camera, mockDevice, _) = createCamera() + + let targetOffset = CGFloat(1.0) + + var setExposureTargetBiasCalled = false + mockDevice.setExposureTargetBiasStub = { bias, handler in + XCTAssertEqual(bias, Float(targetOffset)) + setExposureTargetBiasCalled = true + } + + camera.setExposureOffset(targetOffset) + + XCTAssertTrue(setExposureTargetBiasCalled) + } + + func testMaximumExposureOffset_returnsDeviceMaxExposureTargetBias() { + let (camera, mockDevice, _) = createCamera() + + let targetOffset = CGFloat(1.0) + + mockDevice.maxExposureTargetBias = Float(targetOffset) + + XCTAssertEqual(camera.maximumExposureOffset, targetOffset) + } + + func testMinimumExposureOffset_returnsDeviceMinExposureTargetBias() { + let (camera, mockDevice, _) = createCamera() + + let targetOffset = CGFloat(1.0) + + mockDevice.minExposureTargetBias = Float(targetOffset) + + XCTAssertEqual(camera.minimumExposureOffset, targetOffset) + } +} diff --git a/camera_avfoundation/darwin/Tests/CameraImageStreamScalerTests.swift b/camera_avfoundation/darwin/Tests/CameraImageStreamScalerTests.swift new file mode 100644 index 0000000..6318857 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraImageStreamScalerTests.swift @@ -0,0 +1,43 @@ +// Copyright 2026 twonly contributors. BSD-style license; see LICENSE. + +import AVFoundation +import XCTest + +@testable import camera_avfoundation + +final class CameraImageStreamScalerTests: XCTestCase { + func testReducesBGRAFrameWithoutChangingPreviewBuffer() throws { + var pixelBuffer: CVPixelBuffer? + XCTAssertEqual( + CVPixelBufferCreate( + kCFAllocatorDefault, 1920, 1080, kCVPixelFormatType_32BGRA, nil, &pixelBuffer), + kCVReturnSuccess) + let buffer = try XCTUnwrap(pixelBuffer) + CVPixelBufferLockBaseAddress(buffer, []) + defer { CVPixelBufferUnlockBaseAddress(buffer, []) } + let address = try XCTUnwrap(CVPixelBufferGetBaseAddress(buffer)) + memset(address, 127, CVPixelBufferGetBytesPerRow(buffer) * 1080) + + let plane = try XCTUnwrap(CameraImageStreamScaler.scaleBGRA(buffer, maximumDimension: 640)) + XCTAssertEqual(plane.width, 640) + XCTAssertEqual(plane.height, 360) + XCTAssertEqual(plane.bytesPerRow, 2560) + XCTAssertEqual(plane.bytes.data.count, 640 * 360 * 4) + XCTAssertEqual(plane.bytes.data.first, 127) + XCTAssertEqual(CVPixelBufferGetWidth(buffer), 1920) + XCTAssertEqual(CVPixelBufferGetHeight(buffer), 1080) + XCTAssertEqual(address.load(as: UInt8.self), 127) + } + + func testDoesNotUpscaleSmallFrames() throws { + var pixelBuffer: CVPixelBuffer? + XCTAssertEqual( + CVPixelBufferCreate( + kCFAllocatorDefault, 320, 240, kCVPixelFormatType_32BGRA, nil, &pixelBuffer), + kCVReturnSuccess) + let buffer = try XCTUnwrap(pixelBuffer) + CVPixelBufferLockBaseAddress(buffer, []) + defer { CVPixelBufferUnlockBaseAddress(buffer, []) } + XCTAssertNil(CameraImageStreamScaler.scaleBGRA(buffer, maximumDimension: 640)) + } +} diff --git a/camera_avfoundation/darwin/Tests/CameraInitRaceConditionsTests.swift b/camera_avfoundation/darwin/Tests/CameraInitRaceConditionsTests.swift new file mode 100644 index 0000000..03f5fa3 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraInitRaceConditionsTests.swift @@ -0,0 +1,92 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import XCTest + +@testable import camera_avfoundation + +final class CameraInitRaceConditionsTests: XCTestCase { + private func createCameraPlugin() -> (CameraPlugin, DispatchQueue) { + let captureSessionQueue = DispatchQueue(label: "io.flutter.camera.captureSessionQueue") + + let cameraPlugin = CameraPlugin( + registry: MockFlutterTextureRegistry(), + messenger: MockFlutterBinaryMessenger(), + globalAPI: MockGlobalEventApi(), + deviceDiscoverer: MockCameraDeviceDiscoverer(), + permissionManager: MockCameraPermissionManager(), + deviceFactory: { _ in MockCaptureDevice() }, + captureSessionFactory: { MockCaptureSession() }, + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: captureSessionQueue + ) + + return (cameraPlugin, captureSessionQueue) + } + + func testFixForCaptureSessionQueueNullPointerCrashDueToRaceCondition() { + let (cameraPlugin, captureSessionQueue) = createCameraPlugin() + let disposeExpectation = expectation(description: "dispose's result block must be called") + let createExpectation = expectation(description: "create's result block must be called") + + // Mimic a dispose call followed by a create call, which can be triggered by slightly dragging the + // home bar, causing the app to be inactive, and immediately regain active. + cameraPlugin.dispose(cameraId: 0) { error in + disposeExpectation.fulfill() + } + + cameraPlugin.createCameraOnSessionQueue( + withName: "acamera", + settings: PlatformMediaSettings( + resolutionPreset: .medium, + framesPerSecond: nil, + videoBitrate: nil, + audioBitrate: nil, + enableAudio: true + ) + ) { result in + createExpectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + // `captureSessionQueue` must not be nil after `create` call. Otherwise a nil + // `captureSessionQueue` passed into `AVCaptureVideoDataOutput::setSampleBufferDelegate:queue:` + // API will cause a crash. + XCTAssertNotNil( + captureSessionQueue, "captureSessionQueue must not be nil after create method.") + } + + func testFlutterChannelInitializedWhenStartingImageStream() { + let (cameraPlugin, _captureSessionQueue) = createCameraPlugin() + let createExpectation = expectation(description: "create's result block must be called") + + cameraPlugin.createCameraOnSessionQueue( + withName: "acamera", + settings: PlatformMediaSettings( + resolutionPreset: .medium, + framesPerSecond: nil, + videoBitrate: nil, + audioBitrate: nil, + enableAudio: true + ) + ) { result in + createExpectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + // Start stream and wait for its completion. + let startStreamExpectation = expectation( + description: "startImageStream's result block must be called") + cameraPlugin.startImageStream(completion: { + _ in + startStreamExpectation.fulfill() + }) + + waitForExpectations(timeout: 30, handler: nil) + XCTAssertEqual(cameraPlugin.camera?.isStreamingImages, true) + } + +} diff --git a/camera_avfoundation/darwin/Tests/CameraMethodChannelTests.swift b/camera_avfoundation/darwin/Tests/CameraMethodChannelTests.swift new file mode 100644 index 0000000..8023139 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraMethodChannelTests.swift @@ -0,0 +1,80 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import XCTest + +@testable import camera_avfoundation + +final class CameraMethodChannelTests: XCTestCase { + private func createCameraPlugin(with session: MockCaptureSession) -> CameraPlugin { + return CameraPlugin( + registry: MockFlutterTextureRegistry(), + messenger: MockFlutterBinaryMessenger(), + globalAPI: MockGlobalEventApi(), + deviceDiscoverer: MockCameraDeviceDiscoverer(), + permissionManager: MockCameraPermissionManager(), + deviceFactory: { _ in MockCaptureDevice() }, + captureSessionFactory: { session }, + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue") + ) + } + + func testCreate_ShouldCallResultOnMainThread() { + let avCaptureSessionMock = MockCaptureSession() + avCaptureSessionMock.canSetSessionPresetStub = { _ in true } + let camera = createCameraPlugin(with: avCaptureSessionMock) + let expectation = self.expectation(description: "Result finished") + + var resultValue: Int64? + camera.createCameraOnSessionQueue( + withName: "acamera", + settings: PlatformMediaSettings( + resolutionPreset: .medium, + framesPerSecond: nil, + videoBitrate: nil, + audioBitrate: nil, + enableAudio: true + ) + ) { result in + resultValue = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + XCTAssertNotNil(resultValue) + } + + func testDisposeShouldDeallocCamera() { + let avCaptureSessionMock = MockCaptureSession() + avCaptureSessionMock.canSetSessionPresetStub = { _ in true } + let camera = createCameraPlugin(with: avCaptureSessionMock) + let createExpectation = self.expectation(description: "create's result block must be called") + + camera.createCameraOnSessionQueue( + withName: "acamera", + settings: PlatformMediaSettings( + resolutionPreset: .medium, + framesPerSecond: nil, + videoBitrate: nil, + audioBitrate: nil, + enableAudio: true + ) + ) { result in + createExpectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + XCTAssertNotNil(camera.camera) + + let disposeExpectation = self.expectation(description: "dispose's result block must be called") + camera.dispose(cameraId: 0) { error in + disposeExpectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + XCTAssertNil(camera.camera, "camera should be deallocated after dispose") + } +} diff --git a/camera_avfoundation/darwin/Tests/CameraOrientationTests.swift b/camera_avfoundation/darwin/Tests/CameraOrientationTests.swift new file mode 100644 index 0000000..4878cbe --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraOrientationTests.swift @@ -0,0 +1,168 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import Flutter +import XCTest + +@testable import camera_avfoundation + +private final class MockUIDevice: UIDevice { + var mockOrientation: UIDeviceOrientation = .unknown + + override var orientation: UIDeviceOrientation { + return mockOrientation + } +} + +final class CameraOrientationTests: XCTestCase { + private func createCameraPlugin() -> ( + cameraPlugin: CameraPlugin, + mockCamera: MockCamera, + mockEventAPI: MockGlobalEventApi, + mockDevice: MockCaptureDevice, + mockDeviceDiscoverer: MockCameraDeviceDiscoverer, + captureSessionQueue: DispatchQueue + ) { + let mockDevice = MockCaptureDevice() + let mockCamera = MockCamera() + let mockEventAPI = MockGlobalEventApi() + let mockDeviceDiscoverer = MockCameraDeviceDiscoverer() + let captureSessionQueue = DispatchQueue(label: "io.flutter.camera.captureSessionQueue") + + let cameraPlugin = CameraPlugin( + registry: MockFlutterTextureRegistry(), + messenger: MockFlutterBinaryMessenger(), + globalAPI: mockEventAPI, + deviceDiscoverer: mockDeviceDiscoverer, + permissionManager: MockCameraPermissionManager(), + deviceFactory: { _ in mockDevice }, + captureSessionFactory: { MockCaptureSession() }, + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: captureSessionQueue + ) + cameraPlugin.camera = mockCamera + + return ( + cameraPlugin, + mockCamera, + mockEventAPI, + mockDevice, + mockDeviceDiscoverer, + captureSessionQueue + ) + } + + private func sendOrientation( + _ orientation: UIDeviceOrientation, + to cameraPlugin: CameraPlugin, + captureSessionQueue: DispatchQueue + ) { + cameraPlugin.orientationChanged(createMockNotification(for: orientation)) + waitForQueueRoundTrip(with: captureSessionQueue) + } + + private func createMockNotification(for deviceOrientation: UIDeviceOrientation) -> Notification { + let mockDevice = MockUIDevice() + mockDevice.mockOrientation = deviceOrientation + return Notification(name: Notification.Name("orientation_test"), object: mockDevice) + } + + func testOrientationNotifications() { + let (cameraPlugin, _, mockEventAPI, _, _, captureSessionQueue) = createCameraPlugin() + + sendOrientation(.portraitUpsideDown, to: cameraPlugin, captureSessionQueue: captureSessionQueue) + XCTAssertEqual(mockEventAPI.lastOrientation, .portraitDown) + sendOrientation(.portrait, to: cameraPlugin, captureSessionQueue: captureSessionQueue) + XCTAssertEqual(mockEventAPI.lastOrientation, .portraitUp) + sendOrientation(.landscapeLeft, to: cameraPlugin, captureSessionQueue: captureSessionQueue) + XCTAssertEqual(mockEventAPI.lastOrientation, .landscapeLeft) + sendOrientation(.landscapeRight, to: cameraPlugin, captureSessionQueue: captureSessionQueue) + XCTAssertEqual(mockEventAPI.lastOrientation, .landscapeRight) + } + + func testOrientationNotificationsNotCalledForFaceUp() { + let (cameraPlugin, _, mockEventAPI, _, _, captureSessionQueue) = createCameraPlugin() + sendOrientation(.faceUp, to: cameraPlugin, captureSessionQueue: captureSessionQueue) + XCTAssertFalse(mockEventAPI.deviceOrientationChangedCalled) + } + + func testOrientationNotificationsNotCalledForFaceDown() { + let (cameraPlugin, _, mockEventAPI, _, _, captureSessionQueue) = createCameraPlugin() + sendOrientation(.faceDown, to: cameraPlugin, captureSessionQueue: captureSessionQueue) + XCTAssertFalse(mockEventAPI.deviceOrientationChangedCalled) + } + + func testOrientationUpdateMustBeOnCaptureSessionQueue() { + let queueExpectation = expectation( + description: "Orientation update must happen on the capture session queue") + let (cameraPlugin, mockCamera, _, _, _, captureSessionQueue) = createCameraPlugin() + let captureSessionQueueSpecific = DispatchSpecificKey() + captureSessionQueue.setSpecific( + key: captureSessionQueueSpecific, + value: ()) + + mockCamera.setDeviceOrientationStub = { orientation in + if DispatchQueue.getSpecific(key: captureSessionQueueSpecific) != nil { + queueExpectation.fulfill() + } + } + + cameraPlugin.orientationChanged(createMockNotification(for: .landscapeLeft)) + waitForExpectations(timeout: 30, handler: nil) + } + + func testOrientationChangedNoRetainCycle() { + let (_, mockCamera, mockEventAPI, mockDevice, mockDeviceDiscoverer, _) = createCameraPlugin() + let captureSessionQueue = DispatchQueue(label: "capture_session_queue") + weak var weakPlugin: CameraPlugin? + weak var weakDevice = mockDevice + + autoreleasepool { + let cameraPlugin = CameraPlugin( + registry: MockFlutterTextureRegistry(), + messenger: MockFlutterBinaryMessenger(), + globalAPI: mockEventAPI, + deviceDiscoverer: mockDeviceDiscoverer, + permissionManager: MockCameraPermissionManager(), + deviceFactory: { _ in weakDevice! }, + captureSessionFactory: { MockCaptureSession() }, + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: captureSessionQueue + ) + weakPlugin = cameraPlugin + cameraPlugin.camera = mockCamera + + cameraPlugin.orientationChanged(createMockNotification(for: .landscapeLeft)) + } + + // Sanity check. + let cameraDeallocatedExpectation = self.expectation( + description: "Camera must have been deallocated.") + captureSessionQueue.async { + XCTAssertNil(weakPlugin) + cameraDeallocatedExpectation.fulfill() + } + // Awaiting expectation is needed. The test is flaky when checking for nil right away. + waitForExpectations(timeout: 1, handler: nil) + + var setDeviceOrientationCalled = false + mockCamera.setDeviceOrientationStub = { orientation in + if orientation == .landscapeLeft { + setDeviceOrientationCalled = true + } + } + + weak var weakEventAPI = mockEventAPI + // Must check in captureSessionQueue since orientationChanged dispatches to this queue. + let expectation = self.expectation(description: "Dispatched to capture session queue") + captureSessionQueue.async { + XCTAssertFalse(setDeviceOrientationCalled) + XCTAssertFalse(weakEventAPI?.deviceOrientationChangedCalled ?? false) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } +} diff --git a/camera_avfoundation/darwin/Tests/CameraPermissionTests.swift b/camera_avfoundation/darwin/Tests/CameraPermissionTests.swift new file mode 100644 index 0000000..d8ca79f --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraPermissionTests.swift @@ -0,0 +1,237 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import Flutter +import XCTest + +@testable import camera_avfoundation + +private final class MockPermissionService: NSObject, PermissionServicing { + var authorizationStatusStub: ((AVMediaType) -> AVAuthorizationStatus)? + var requestAccessStub: ((AVMediaType, @escaping @Sendable (Bool) -> Void) -> Void)? + + func authorizationStatus(for mediaType: AVMediaType) -> AVAuthorizationStatus { + return authorizationStatusStub?(mediaType) ?? .notDetermined + } + + func requestAccess(for mediaType: AVMediaType, completion: @escaping @Sendable (Bool) -> Void) { + requestAccessStub?(mediaType, completion) + } +} + +final class CameraPermissionManagerTests: XCTestCase { + private func createSutAndMocks() -> (CameraPermissionManager, MockPermissionService) { + let mockPermissionService = MockPermissionService() + let permissionManager = CameraPermissionManager(permissionService: mockPermissionService) + + return (permissionManager, mockPermissionService) + } + + // MARK: - Camera permissions + + func testRequestCameraPermission_completeWithoutErrorIfPreviouslyAuthorized() { + let (permissionManager, mockPermissionService) = createSutAndMocks() + let expectation = self.expectation( + description: "Must complete without error if camera access was previously authorized.") + + mockPermissionService.authorizationStatusStub = { mediaType in + XCTAssertEqual(mediaType, .video) + return .authorized + } + permissionManager.requestCameraPermission { error in + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testRequestCameraPermission_completeWithErrorIfPreviouslyDenied() { + let (permissionManager, mockPermissionService) = createSutAndMocks() + let expectation = self.expectation( + description: "Must complete with error if camera access was previously denied.") + + mockPermissionService.authorizationStatusStub = { mediaType in + XCTAssertEqual(mediaType, .video) + return .denied + } + permissionManager.requestCameraPermission { error in + XCTAssertEqual(error?.code, "CameraAccessDeniedWithoutPrompt") + XCTAssertEqual( + error?.message, + "User has previously denied the camera access request. Go to Settings to enable camera access." + ) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testRequestCameraPermission_completeWithErrorIfRestricted() { + let (permissionManager, mockPermissionService) = createSutAndMocks() + let expectation = self.expectation( + description: "Must complete with error if camera access is restricted.") + + mockPermissionService.authorizationStatusStub = { mediaType in + XCTAssertEqual(mediaType, .video) + return .restricted + } + permissionManager.requestCameraPermission { error in + XCTAssertEqual(error?.code, "CameraAccessRestricted") + XCTAssertEqual(error?.message, "Camera access is restricted.") + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testRequestCameraPermission_completeWithoutErrorIfUserGrantAccess() { + let (permissionManager, mockPermissionService) = createSutAndMocks() + let expectation = self.expectation( + description: "Must complete without error if user granted access.") + + mockPermissionService.authorizationStatusStub = { mediaType in + XCTAssertEqual(mediaType, .video) + return .notDetermined + } + mockPermissionService.requestAccessStub = { mediaType, handler in + XCTAssertEqual(mediaType, .video) + // Grant access. + handler(true) + } + permissionManager.requestCameraPermission { error in + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testRequestCameraPermission_completeWithErrorIfUserDenyAccess() { + let (permissionManager, mockPermissionService) = createSutAndMocks() + let expectation = self.expectation( + description: "Must complete with error if user denied access.") + + mockPermissionService.authorizationStatusStub = { mediaType in + XCTAssertEqual(mediaType, .video) + return .notDetermined + } + mockPermissionService.requestAccessStub = { mediaType, handler in + XCTAssertEqual(mediaType, .video) + // Deny access. + handler(false) + } + permissionManager.requestCameraPermission { error in + XCTAssertEqual(error?.code, "CameraAccessDenied") + XCTAssertEqual(error?.message, "User denied the camera access request.") + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } + + // MARK: - Audio permissions + + func testRequestAudioPermission_completeWithoutErrorIfPreviouslyAuthorized() { + let (permissionManager, mockPermissionService) = createSutAndMocks() + let expectation = self.expectation( + description: "Must complete without error if audio access was previously authorized.") + + mockPermissionService.authorizationStatusStub = { mediaType in + XCTAssertEqual(mediaType, .audio) + return .authorized + } + permissionManager.requestAudioPermission { error in + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testRequestAudioPermission_completeWithErrorIfPreviouslyDenied() { + let (permissionManager, mockPermissionService) = createSutAndMocks() + let expectation = self.expectation( + description: "Must complete with error if audio access was previously denied.") + + mockPermissionService.authorizationStatusStub = { mediaType in + XCTAssertEqual(mediaType, .audio) + return .denied + } + permissionManager.requestAudioPermission { error in + XCTAssertEqual(error?.code, "AudioAccessDeniedWithoutPrompt") + XCTAssertEqual( + error?.message, + "User has previously denied the audio access request. Go to Settings to enable audio access." + ) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testRequestAudioPermission_completeWithErrorIfRestricted() { + let (permissionManager, mockPermissionService) = createSutAndMocks() + let expectation = self.expectation( + description: "Must complete with error if audio access is restricted.") + + mockPermissionService.authorizationStatusStub = { mediaType in + XCTAssertEqual(mediaType, .audio) + return .restricted + } + permissionManager.requestAudioPermission { error in + XCTAssertEqual(error?.code, "AudioAccessRestricted") + XCTAssertEqual(error?.message, "Audio access is restricted.") + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testRequestAudioPermission_completeWithoutErrorIfUserGrantAccess() { + let (permissionManager, mockPermissionService) = createSutAndMocks() + let expectation = self.expectation( + description: "Must complete without error if user granted access.") + + mockPermissionService.authorizationStatusStub = { mediaType in + XCTAssertEqual(mediaType, .audio) + return .notDetermined + } + mockPermissionService.requestAccessStub = { mediaType, handler in + XCTAssertEqual(mediaType, .audio) + // Grant access. + handler(true) + } + permissionManager.requestAudioPermission { error in + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testRequestAudioPermission_completeWithErrorIfUserDenyAccess() { + let (permissionManager, mockPermissionService) = createSutAndMocks() + let expectation = self.expectation( + description: "Must complete with error if user denied access") + + mockPermissionService.authorizationStatusStub = { mediaType in + XCTAssertEqual(mediaType, .audio) + return .notDetermined + } + mockPermissionService.requestAccessStub = { mediaType, handler in + XCTAssertEqual(mediaType, .audio) + // Deny access. + handler(false) + } + permissionManager.requestAudioPermission { error in + XCTAssertEqual(error?.code, "AudioAccessDenied") + XCTAssertEqual(error?.message, "User denied the audio access request.") + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } +} diff --git a/camera_avfoundation/darwin/Tests/CameraPluginCreateCameraTests.swift b/camera_avfoundation/darwin/Tests/CameraPluginCreateCameraTests.swift new file mode 100644 index 0000000..269ba03 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraPluginCreateCameraTests.swift @@ -0,0 +1,131 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import XCTest + +@testable import camera_avfoundation + +final class CameraPluginCreateCameraTests: XCTestCase { + private func createCameraPlugin() -> ( + CameraPlugin, MockCameraPermissionManager, MockCaptureSession + ) { + let mockPermissionManager = MockCameraPermissionManager() + let mockCaptureSession = MockCaptureSession() + + let cameraPlugin = CameraPlugin( + registry: MockFlutterTextureRegistry(), + messenger: MockFlutterBinaryMessenger(), + globalAPI: MockGlobalEventApi(), + deviceDiscoverer: MockCameraDeviceDiscoverer(), + permissionManager: mockPermissionManager, + deviceFactory: { _ in MockCaptureDevice() }, + captureSessionFactory: { mockCaptureSession }, + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue") + ) + + return (cameraPlugin, mockPermissionManager, mockCaptureSession) + } + + func testCreateCamera_requestsOnlyCameraPermissionWithAudioDisabled() { + let (cameraPlugin, mockPermissionManager, _) = createCameraPlugin() + let expectation = expectation(description: "Initialization completed") + + var requestCameraPermissionCalled = false + mockPermissionManager.requestCameraPermissionStub = { completion in + requestCameraPermissionCalled = true + // Permission is granted + completion(nil) + } + var requestAudioPermissionCalled = false + mockPermissionManager.requestAudioPermissionStub = { completion in + requestAudioPermissionCalled = true + // Permission is granted + completion(nil) + } + + cameraPlugin.create( + cameraName: "camera_name", + settings: PlatformMediaSettings( + resolutionPreset: .medium, + framesPerSecond: nil, + videoBitrate: nil, + audioBitrate: nil, + enableAudio: false) + ) { result in + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(requestCameraPermissionCalled) + XCTAssertFalse(requestAudioPermissionCalled) + } + + func testCreateCamera_requestsCameraAndAudioPermissionWithAudioEnabled() { + let (cameraPlugin, mockPermissionManager, _) = createCameraPlugin() + let expectation = expectation(description: "Initialization completed") + + var requestCameraPermissionCalled = false + mockPermissionManager.requestCameraPermissionStub = { completion in + requestCameraPermissionCalled = true + // Permission is granted + completion(nil) + } + var requestAudioPermissionCalled = false + mockPermissionManager.requestAudioPermissionStub = { completion in + requestAudioPermissionCalled = true + // Permission is granted + completion(nil) + } + + cameraPlugin.create( + cameraName: "camera_name", + settings: PlatformMediaSettings( + resolutionPreset: .medium, + framesPerSecond: nil, + videoBitrate: nil, + audioBitrate: nil, + enableAudio: true) + ) { result in + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(requestCameraPermissionCalled) + XCTAssertTrue(requestAudioPermissionCalled) + } + + func testCreateCamera_createsCameraSuccessfully() { + let (cameraPlugin, mockPermissionManager, mockCaptureSession) = createCameraPlugin() + let expectation = expectation(description: "Initialization completed") + + mockPermissionManager.requestCameraPermissionStub = { completion in + // Permission is granted + completion(nil) + } + mockPermissionManager.requestAudioPermissionStub = { completion in + // Permission is granted + completion(nil) + } + mockCaptureSession.canSetSessionPresetStub = { _ in true } + + cameraPlugin.create( + cameraName: "camera_name", + settings: PlatformMediaSettings( + resolutionPreset: .medium, + framesPerSecond: nil, + videoBitrate: nil, + audioBitrate: nil, + enableAudio: true) + ) { result in + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertNotNil(cameraPlugin.camera) + } +} diff --git a/camera_avfoundation/darwin/Tests/CameraPluginDelegatingMethodTests.swift b/camera_avfoundation/darwin/Tests/CameraPluginDelegatingMethodTests.swift new file mode 100644 index 0000000..37ee555 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraPluginDelegatingMethodTests.swift @@ -0,0 +1,652 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import XCTest + +@testable import camera_avfoundation + +/// Tests of `CameraPlugin` methods delegating to `Camera` instance +final class CameraPluginDelegatingMethodTests: XCTestCase { + private func createCameraPlugin() -> (CameraPlugin, MockCamera) { + let mockCamera = MockCamera() + + let cameraPlugin = CameraPlugin( + registry: MockFlutterTextureRegistry(), + messenger: MockFlutterBinaryMessenger(), + globalAPI: MockGlobalEventApi(), + deviceDiscoverer: MockCameraDeviceDiscoverer(), + permissionManager: MockCameraPermissionManager(), + deviceFactory: { _ in MockCaptureDevice() }, + captureSessionFactory: { MockCaptureSession() }, + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue") + ) + cameraPlugin.camera = mockCamera + + return (cameraPlugin, mockCamera) + } + + func testLockCapture_callsCameraLockCapture() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetOrientation = PlatformDeviceOrientation.landscapeLeft + + var lockCaptureCalled = false + mockCamera.lockCaptureOrientationStub = { orientation in + XCTAssertEqual(orientation, targetOrientation) + lockCaptureCalled = true + } + + cameraPlugin.lockCaptureOrientation(orientation: targetOrientation) { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(lockCaptureCalled) + } + + func testPausePreview_callsCameraPausePreview() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + var pausePreviewCalled = false + mockCamera.pausePreviewStub = { + pausePreviewCalled = true + } + + cameraPlugin.pausePreview { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(pausePreviewCalled) + } + + func testPauseVideoRecording_callsCameraPauseVideoRecording() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + var pauseVideoRecordingCalled = false + mockCamera.pauseVideoRecordingStub = { + pauseVideoRecordingCalled = true + } + + cameraPlugin.pauseVideoRecording { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(pauseVideoRecordingCalled) + } + + func testPrepareForVideoRecording_callsCameraSetUpCaptureSessionForAudioIfNeeded() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + var setUpCaptureSessionForAudioIfNeededCalled = false + mockCamera.setUpCaptureSessionForAudioIfNeededStub = { + setUpCaptureSessionForAudioIfNeededCalled = true + } + + cameraPlugin.prepareForVideoRecording { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(setUpCaptureSessionForAudioIfNeededCalled) + } + + func testReceivedImageStreamData_callsCameraReceivedImageStreamData() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + var receivedImageStreamDataCalled = false + mockCamera.receivedImageStreamDataStub = { + receivedImageStreamDataCalled = true + } + + cameraPlugin.receivedImageStreamData { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(receivedImageStreamDataCalled) + } + + func testResumeVideoRecording_callsCameraResumeVideoRecording() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + var resumeVideoRecordingCalled = false + mockCamera.resumeVideoRecordingStub = { + resumeVideoRecordingCalled = true + } + + cameraPlugin.resumeVideoRecording { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(resumeVideoRecordingCalled) + } + + func testResumePreview_callsCameraResumePreview() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + var resumePreviewCalled = false + mockCamera.resumePreviewStub = { + resumePreviewCalled = true + } + + cameraPlugin.resumePreview { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(resumePreviewCalled) + } + + func testSetExposureMode_callsCameraExposureMode() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetExposureMode = PlatformExposureMode.locked + + var setExposureModeCalled = false + mockCamera.setExposureModeStub = { mode in + XCTAssertEqual(mode, targetExposureMode) + setExposureModeCalled = true + } + + cameraPlugin.setExposureMode(mode: targetExposureMode) { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(setExposureModeCalled) + } + + func testSetExposureOffset_callsCameraSetExposureOffset() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetExposureOffset = 1.0 + + var setExposureOffsetCalled = false + mockCamera.setExposureOffsetStub = { offset in + XCTAssertEqual(offset, targetExposureOffset) + setExposureOffsetCalled = true + } + + cameraPlugin.setExposureOffset(offset: targetExposureOffset) { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(setExposureOffsetCalled) + } + + func testSetFocusMode_callsCameraSetFocusMode() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetFocusMode = PlatformFocusMode.locked + + var setFocusModeCalled = false + mockCamera.setFocusModeStub = { mode in + XCTAssertEqual(mode, targetFocusMode) + setFocusModeCalled = true + } + + cameraPlugin.setFocusMode(mode: targetFocusMode) { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(setFocusModeCalled) + } + + func testSetImageFileFormat_callsCameraSetImageFileFormat() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetFileFormat = PlatformImageFileFormat.heif + + var setImageFileFormatCalled = false + mockCamera.setImageFileFormatStub = { fileFormat in + XCTAssertEqual(fileFormat, targetFileFormat) + setImageFileFormatCalled = true + } + + cameraPlugin.setImageFileFormat(format: targetFileFormat) { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(setImageFileFormatCalled) + } + + func testSetImageQuality_callsCameraSetImageQuality() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetQuality: Int64 = 50 + + var setJpegImageQualityCalled = false + mockCamera.setJpegImageQualityStub = { quality in + XCTAssertEqual(quality, targetQuality) + setJpegImageQualityCalled = true + } + + cameraPlugin.setJpegImageQuality(quality: targetQuality) { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(setJpegImageQualityCalled) + } + + func testStartImageStream_callsCameraStartImageStream() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + var startImageStreamCalled = false + mockCamera.startImageStreamStub = { messenger, completion in + startImageStreamCalled = true + completion(.success(())) + } + + cameraPlugin.startImageStream { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(startImageStreamCalled) + } + + func testStopImageStream_callsCameraStopImageStream() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + var stopImageStreamCalled = false + mockCamera.stopImageStreamStub = { + stopImageStreamCalled = true + } + + cameraPlugin.stopImageStream { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(stopImageStreamCalled) + } + + func testStartVideoRecording_withStreamingTrue_callsCameraStartVideoRecordingWithMessenger() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + var startVideoRecordingCalled = false + mockCamera.startVideoRecordingStub = { completion, messenger in + XCTAssertNotNil(messenger) + startVideoRecordingCalled = true + completion(.success(())) + } + + cameraPlugin.startVideoRecording(enableStream: true) { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(startVideoRecordingCalled) + } + + func testStartVideoRecording_withStreamingFalse_callsCameraStartVideoRecordingWithoutMessenger() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + var startVideoRecordingCalled = false + mockCamera.startVideoRecordingStub = { completion, messenger in + XCTAssertNil(messenger) + startVideoRecordingCalled = true + completion(.success(())) + } + + cameraPlugin.startVideoRecording(enableStream: false) { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(startVideoRecordingCalled) + } + + func testStopVideoRecording_callsCameraStopVideoRecording() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetPath = "path" + + var stopVideoRecordingCalled = false + mockCamera.stopVideoRecordingStub = { completion in + stopVideoRecordingCalled = true + completion(.success(targetPath)) + } + + cameraPlugin.stopVideoRecording { result in + switch result { + case .success(let path): + XCTAssertEqual(path, targetPath) + case .failure: + XCTFail("Unexpected error") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(stopVideoRecordingCalled) + } + + func testUnlockCaptureOrientation_callsCameraUnlockCaptureOrientation() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + var unlockCaptureOrientationCalled = false + mockCamera.unlockCaptureOrientationStub = { + unlockCaptureOrientationCalled = true + } + + cameraPlugin.unlockCaptureOrientation { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(unlockCaptureOrientationCalled) + } + + func testSetExposurePoint_callsCameraSetExposurePoint() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetExposurePoint = PlatformPoint(x: 1.0, y: 1.0) + + var setExposurePointCalled = false + mockCamera.setExposurePointStub = { point, completion in + XCTAssertEqual(point, targetExposurePoint) + setExposurePointCalled = true + completion(.success(())) + } + + cameraPlugin.setExposurePoint(point: targetExposurePoint) { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(setExposurePointCalled) + } + + func testSetFlashMode_callsCameraSetFlashMode() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetFlashMode = PlatformFlashMode.auto + + var setFlashModeCalled = false + mockCamera.setFlashModeStub = { mode, completion in + XCTAssertEqual(mode, targetFlashMode) + setFlashModeCalled = true + completion(.success(())) + } + + cameraPlugin.setFlashMode(mode: targetFlashMode) { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(setFlashModeCalled) + } + + func testSetFocusPoint_callsCameraSetFocusPoint() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetFocusPoint = PlatformPoint(x: 1.0, y: 1.0) + + var setFocusPointCalled = false + mockCamera.setFocusPointStub = { point, completion in + XCTAssertEqual(point, targetFocusPoint) + setFocusPointCalled = true + completion(.success(())) + } + + cameraPlugin.setFocusPoint(point: targetFocusPoint) { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(setFocusPointCalled) + } + + func testSetZoomLevel_callsCameraSetZoomLevel() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetZoomLevel = 1.0 + + var setZoomLevelCalled = false + mockCamera.setZoomLevelStub = { zoom, completion in + XCTAssertEqual(zoom, targetZoomLevel) + setZoomLevelCalled = true + completion(.success(())) + } + + cameraPlugin.setZoomLevel(zoom: targetZoomLevel) { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(setZoomLevelCalled) + } + + func testTakePicture_callsCameraCaptureToFile() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetPath = "path" + + var captureToFileCalled = false + mockCamera.captureToFileStub = { completion in + captureToFileCalled = true + completion(.success(targetPath)) + } + + cameraPlugin.takePicture { result in + switch result { + case .success(let path): + XCTAssertEqual(path, targetPath) + case .failure: + XCTFail("Unexpected error") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(captureToFileCalled) + } + + func testUpdateDescriptionWhileRecording_callsCameraSetDescriptionWhileRecording() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetCameraName = "camera_name" + + var setDescriptionWhileRecordingCalled = false + mockCamera.setDescriptionWhileRecordingStub = { cameraName, completion in + XCTAssertEqual(cameraName, targetCameraName) + setDescriptionWhileRecordingCalled = true + completion(.success(())) + } + + cameraPlugin.updateDescriptionWhileRecording(cameraName: targetCameraName) { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(setDescriptionWhileRecordingCalled) + } + + func testGetMaxZoomLevel_returnsValueFromCameraGetMaximumAvailableZoomFactor() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetMaximumZoomLevel = CGFloat(1.0) + + var getMaximumAvailableZoomFactorCalled = false + mockCamera.getMaximumAvailableZoomFactorStub = { + getMaximumAvailableZoomFactorCalled = true + return targetMaximumZoomLevel + } + + cameraPlugin.getMaxZoomLevel { result in + switch result { + case .success(let zoom): + XCTAssertEqual(zoom, targetMaximumZoomLevel) + case .failure: + XCTFail("Unexpected error") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(getMaximumAvailableZoomFactorCalled) + } + + func testGetMinZoomLevel_returnsValueFromCameraGetMinimumAvailableZoomFactor() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetMinimumZoomLevel = CGFloat(1.0) + + var getMinimumAvailableZoomFactorCalled = false + mockCamera.getMinimumAvailableZoomFactorStub = { + getMinimumAvailableZoomFactorCalled = true + return targetMinimumZoomLevel + } + + cameraPlugin.getMinZoomLevel { result in + switch result { + case .success(let zoom): + XCTAssertEqual(zoom, targetMinimumZoomLevel) + case .failure: + XCTFail("Unexpected error") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(getMinimumAvailableZoomFactorCalled) + } + + func testGetMaxExposureOffset_returnsValueFromCameraGetMaximumExposureOffset() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetMaximumExposureOffset = CGFloat(1.0) + + var getMaximumExposureOffsetCalled = false + mockCamera.getMaximumExposureOffsetStub = { + getMaximumExposureOffsetCalled = true + return targetMaximumExposureOffset + } + + cameraPlugin.getMaxExposureOffset { result in + switch result { + case .success(let offset): + XCTAssertEqual(offset, targetMaximumExposureOffset) + case .failure: + XCTFail("Unexpected error") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(getMaximumExposureOffsetCalled) + } + + func testGetMinExposureOffset_returnsValueFromCameraGetMinimumExposureOffset() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetMinimumExposureOffset = CGFloat(1.0) + + var getMinimumExposureOffsetCalled = false + mockCamera.getMinimumExposureOffsetStub = { + getMinimumExposureOffsetCalled = true + return targetMinimumExposureOffset + } + + cameraPlugin.getMinExposureOffset { result in + switch result { + case .success(let offset): + XCTAssertEqual(offset, targetMinimumExposureOffset) + case .failure: + XCTFail("Unexpected error") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(getMinimumExposureOffsetCalled) + } +} diff --git a/camera_avfoundation/darwin/Tests/CameraPluginInitializeCameraTests.swift b/camera_avfoundation/darwin/Tests/CameraPluginInitializeCameraTests.swift new file mode 100644 index 0000000..979ff89 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraPluginInitializeCameraTests.swift @@ -0,0 +1,105 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import XCTest + +@testable import camera_avfoundation + +final class CameraPluginInitializeCameraTests: XCTestCase { + private func createCameraPlugin() -> ( + CameraPlugin, MockCamera, MockGlobalEventApi, DispatchQueue + ) { + let mockCamera = MockCamera() + let mockGlobalEventApi = MockGlobalEventApi() + let captureSessionQueue = DispatchQueue(label: "io.flutter.camera.captureSessionQueue") + + let cameraPlugin = CameraPlugin( + registry: MockFlutterTextureRegistry(), + messenger: MockFlutterBinaryMessenger(), + globalAPI: mockGlobalEventApi, + deviceDiscoverer: MockCameraDeviceDiscoverer(), + permissionManager: MockCameraPermissionManager(), + deviceFactory: { _ in MockCaptureDevice() }, + captureSessionFactory: { MockCaptureSession() }, + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: captureSessionQueue + ) + cameraPlugin.camera = mockCamera + + return (cameraPlugin, mockCamera, mockGlobalEventApi, captureSessionQueue) + } + + func testInitializeCamera_setsCameraOnFrameAvailableCallback() { + let (cameraPlugin, mockCamera, _, _) = createCameraPlugin() + let expectation = expectation(description: "Initialization completed") + + var onFrameAvailableSet = false + mockCamera.setOnFrameAvailableStub = { callback in + onFrameAvailableSet = true + } + + cameraPlugin.initialize(cameraId: 0, imageFormat: PlatformImageFormatGroup.bgra8888) { + result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(onFrameAvailableSet) + } + + func testInitializeCamera_setsCameraDartAPI() { + let (cameraPlugin, mockCamera, _, _) = createCameraPlugin() + let expectation = expectation(description: "Initialization completed") + + var dartAPISet = false + mockCamera.setDartApiStub = { api in + dartAPISet = true + } + + cameraPlugin.initialize(cameraId: 0, imageFormat: PlatformImageFormatGroup.bgra8888) { + result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(dartAPISet) + } + + func testInitializeCamera_sendsDeviceOrientation() { + let (cameraPlugin, _, mockGlobalEventApi, captureSessionQueue) = createCameraPlugin() + + cameraPlugin.initialize(cameraId: 0, imageFormat: PlatformImageFormatGroup.bgra8888) { + result in + let _ = self.assertSuccess(result) + } + + waitForQueueRoundTrip(with: captureSessionQueue) + + XCTAssertTrue(mockGlobalEventApi.deviceOrientationChangedCalled) + } + + func testInitializeCamera_startsCamera() { + let (cameraPlugin, mockCamera, _, _) = createCameraPlugin() + let expectation = expectation(description: "Initialization completed") + + var startCalled = false + mockCamera.startStub = { + startCalled = true + } + + cameraPlugin.initialize(cameraId: 0, imageFormat: PlatformImageFormatGroup.bgra8888) { + result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(startCalled) + } +} diff --git a/camera_avfoundation/darwin/Tests/CameraPreviewPauseTests.swift b/camera_avfoundation/darwin/Tests/CameraPreviewPauseTests.swift new file mode 100644 index 0000000..7803ec3 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraPreviewPauseTests.swift @@ -0,0 +1,26 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import XCTest + +@testable import camera_avfoundation + +final class CameraPreviewPauseTests: XCTestCase { + func testPausePreviewWithResult_shouldPausePreview() { + let camera = CameraTestUtils.createTestCamera() + + camera.pausePreview() + + XCTAssertTrue(camera.isPreviewPaused) + } + + func testResumePreviewWithResult_shouldResumePreview() { + let camera = CameraTestUtils.createTestCamera() + + camera.resumePreview() + + XCTAssertFalse(camera.isPreviewPaused) + } +} diff --git a/camera_avfoundation/darwin/Tests/CameraPropertiesTests.swift b/camera_avfoundation/darwin/Tests/CameraPropertiesTests.swift new file mode 100644 index 0000000..bac0ce6 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraPropertiesTests.swift @@ -0,0 +1,73 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import Foundation +import XCTest + +@testable import camera_avfoundation + +final class CameraPropertiesTests: XCTestCase { + // MARK: - Flash Mode Tests + + func testGetAVCaptureFlashModeForPigeonFlashMode() { + XCTAssertEqual( + AVCaptureDevice.FlashMode.off, + getAVCaptureFlashMode(for: .off)) + XCTAssertEqual( + AVCaptureDevice.FlashMode.auto, + getAVCaptureFlashMode(for: .auto)) + XCTAssertEqual( + AVCaptureDevice.FlashMode.on, + getAVCaptureFlashMode(for: .always)) + } + + // MARK: - Video Format Tests + + func testGetPixelFormatForPigeonFormat() { + XCTAssertEqual( + kCVPixelFormatType_32BGRA, + getPixelFormat(for: .bgra8888)) + XCTAssertEqual( + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + getPixelFormat(for: .yuv420)) + } + + // MARK: - Device Orientation Tests + + func testGetUIDeviceOrientationForPigeonDeviceOrientation() { + XCTAssertEqual( + UIDeviceOrientation.portraitUpsideDown, + getUIDeviceOrientation(for: .portraitDown) + ) + XCTAssertEqual( + UIDeviceOrientation.landscapeLeft, + getUIDeviceOrientation(for: .landscapeLeft)) + XCTAssertEqual( + UIDeviceOrientation.landscapeRight, + getUIDeviceOrientation(for: .landscapeRight)) + XCTAssertEqual( + UIDeviceOrientation.portrait, + getUIDeviceOrientation(for: .portraitUp)) + } + + func testGetPigeonDeviceOrientationForUIDeviceOrientation() { + XCTAssertEqual( + PlatformDeviceOrientation.portraitDown, + getPigeonDeviceOrientation(for: .portraitUpsideDown)) + XCTAssertEqual( + PlatformDeviceOrientation.landscapeLeft, + getPigeonDeviceOrientation(for: .landscapeLeft)) + XCTAssertEqual( + PlatformDeviceOrientation.landscapeRight, + getPigeonDeviceOrientation(for: .landscapeRight)) + XCTAssertEqual( + PlatformDeviceOrientation.portraitUp, + getPigeonDeviceOrientation(for: .portrait)) + // Test default case. + XCTAssertEqual( + PlatformDeviceOrientation.portraitUp, + getPigeonDeviceOrientation(for: .unknown)) + } +} diff --git a/camera_avfoundation/darwin/Tests/CameraSessionPresetsTests.swift b/camera_avfoundation/darwin/Tests/CameraSessionPresetsTests.swift new file mode 100644 index 0000000..5d56567 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraSessionPresetsTests.swift @@ -0,0 +1,97 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import XCTest + +@testable import camera_avfoundation + +/// Includes test cases related to resolution presets setting operations for Camera class. +final class CameraSessionPresetsTests: XCTestCase { + func testResolutionPresetWithBestFormat_mustUpdateCaptureSessionPreset() { + let expectedPreset = AVCaptureSession.Preset.inputPriority + let presetExpectation = expectation(description: "Expected preset set") + let lockForConfigurationExpectation = expectation( + description: "Expected lockForConfiguration called") + + let videoSessionMock = MockCaptureSession() + videoSessionMock.canSetSessionPresetStub = { _ in true } + videoSessionMock.setSessionPresetStub = { preset in + if preset == expectedPreset { + presetExpectation.fulfill() + } + } + let captureFormatMock = MockCaptureDeviceFormat() + let captureDeviceMock = MockCaptureDevice() + captureDeviceMock.flutterFormats = [captureFormatMock] + var currentFormat: CaptureDeviceFormat = captureFormatMock + captureDeviceMock.activeFormatStub = { + return currentFormat + } + captureDeviceMock.lockForConfigurationStub = { + lockForConfigurationExpectation.fulfill() + } + + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.videoCaptureDeviceFactory = { _ in captureDeviceMock } + configuration.videoDimensionsConverter = { _ in + return CMVideoDimensions(width: 4, height: 3) + } + configuration.videoCaptureSession = videoSessionMock + configuration.mediaSettings = CameraTestUtils.createDefaultMediaSettings( + resolutionPreset: PlatformResolutionPreset.max) + + let _ = CameraTestUtils.createTestCamera(configuration) + + waitForExpectations(timeout: 30, handler: nil) + } + + func testResolutionPresetWithCanSetSessionPresetMax_mustUpdateCaptureSessionPreset() { + let expectedPreset = AVCaptureSession.Preset.hd4K3840x2160 + let expectation = self.expectation(description: "Expected preset set") + + let videoSessionMock = MockCaptureSession() + // Make sure that setting resolution preset for session always succeeds. + videoSessionMock.canSetSessionPresetStub = { _ in true } + videoSessionMock.setSessionPresetStub = { preset in + if preset == expectedPreset { + expectation.fulfill() + } + } + + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.videoCaptureSession = videoSessionMock + configuration.mediaSettings = CameraTestUtils.createDefaultMediaSettings( + resolutionPreset: PlatformResolutionPreset.max) + configuration.videoCaptureDeviceFactory = { _ in MockCaptureDevice() } + + let _ = CameraTestUtils.createTestCamera(configuration) + + waitForExpectations(timeout: 30, handler: nil) + } + + func testResolutionPresetWithCanSetSessionPresetUltraHigh_mustUpdateCaptureSessionPreset() { + let expectedPreset = AVCaptureSession.Preset.hd4K3840x2160 + let expectation = self.expectation(description: "Expected preset set") + + let videoSessionMock = MockCaptureSession() + // Make sure that setting resolution preset for session always succeeds. + videoSessionMock.canSetSessionPresetStub = { _ in true } + // Expect that setting "ultraHigh" resolutionPreset correctly updates videoCaptureSession. + videoSessionMock.setSessionPresetStub = { preset in + if preset == expectedPreset { + expectation.fulfill() + } + } + + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.videoCaptureSession = videoSessionMock + configuration.mediaSettings = CameraTestUtils.createDefaultMediaSettings( + resolutionPreset: PlatformResolutionPreset.ultraHigh) + + let _ = CameraTestUtils.createTestCamera(configuration) + + waitForExpectations(timeout: 30, handler: nil) + } +} diff --git a/camera_avfoundation/darwin/Tests/CameraSetDeviceOrientationTests.swift b/camera_avfoundation/darwin/Tests/CameraSetDeviceOrientationTests.swift new file mode 100644 index 0000000..c6ca992 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraSetDeviceOrientationTests.swift @@ -0,0 +1,108 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import XCTest + +@testable import camera_avfoundation + +final class CameraSetDeviceOrientationTests: XCTestCase { + private func createCamera() -> (Camera, MockCaptureConnection, MockCaptureConnection) { + let camera = CameraTestUtils.createTestCamera() + + let mockCapturePhotoOutput = MockCapturePhotoOutput() + let mockPhotoCaptureConnection = MockCaptureConnection() + mockPhotoCaptureConnection.isVideoOrientationSupported = true + + mockCapturePhotoOutput.connectionWithMediaTypeStub = { _ in mockPhotoCaptureConnection } + camera.capturePhotoOutput = mockCapturePhotoOutput + + let mockCaptureVideoDataOutput = MockCaptureVideoDataOutput() + let mockVideoCaptureConnection = MockCaptureConnection() + mockVideoCaptureConnection.isVideoOrientationSupported = true + + mockCaptureVideoDataOutput.connectionWithMediaTypeStub = { _ in mockVideoCaptureConnection } + camera.captureVideoOutput = mockCaptureVideoDataOutput + + return (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) + } + + func testSetDeviceOrientation_setsOrientationsOfCaptureConnections() { + let (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) = createCamera() + var photoSetVideoOrientationCalled = false + mockPhotoCaptureConnection.setVideoOrientationStub = { orientation in + // Device orientation is flipped compared to video orientation. When UIDeviceOrientation + // is landscape left the video orientation should be landscape right. + XCTAssertEqual(orientation, .landscapeRight) + photoSetVideoOrientationCalled = true + } + + var videoSetVideoOrientationCalled = false + mockVideoCaptureConnection.setVideoOrientationStub = { orientation in + // Device orientation is flipped compared to video orientation. When UIDeviceOrientation + // is landscape left the video orientation should be landscape right. + XCTAssertEqual(orientation, .landscapeRight) + videoSetVideoOrientationCalled = true + } + + camera.deviceOrientation = .landscapeLeft + + XCTAssertTrue(photoSetVideoOrientationCalled) + XCTAssertTrue(videoSetVideoOrientationCalled) + } + + func + testSetDeviceOrientation_setsLockedOrientationsOfCaptureConnection_ifCaptureOrientationIsLocked() + { + let (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) = createCamera() + var photoSetVideoOrientationCalled = false + mockPhotoCaptureConnection.setVideoOrientationStub = { orientation in + XCTAssertEqual(orientation, .portraitUpsideDown) + photoSetVideoOrientationCalled = true + } + + var videoSetVideoOrientationCalled = false + mockVideoCaptureConnection.setVideoOrientationStub = { orientation in + XCTAssertEqual(orientation, .portraitUpsideDown) + videoSetVideoOrientationCalled = true + } + + camera.lockCaptureOrientation(PlatformDeviceOrientation.portraitDown) + + camera.deviceOrientation = .landscapeLeft + + XCTAssertTrue(photoSetVideoOrientationCalled) + XCTAssertTrue(videoSetVideoOrientationCalled) + } + + func testSetDeviceOrientation_doesNotSetOrientations_ifRecordingIsInProgress() { + let (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) = createCamera() + + camera.startVideoRecording(completion: { _ in }, messengerForStreaming: nil) + + mockPhotoCaptureConnection.setVideoOrientationStub = { _ in XCTFail() } + mockVideoCaptureConnection.setVideoOrientationStub = { _ in XCTFail() } + + camera.deviceOrientation = .landscapeLeft + } + + func testSetDeviceOrientation_doesNotSetOrientations_forDuplicateUpdates() { + let (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) = createCamera() + var photoSetVideoOrientationCallCount = 0 + mockPhotoCaptureConnection.setVideoOrientationStub = { _ in + photoSetVideoOrientationCallCount += 1 + } + + var videoSetVideoOrientationCallCount = 0 + mockVideoCaptureConnection.setVideoOrientationStub = { _ in + videoSetVideoOrientationCallCount += 1 + } + + camera.deviceOrientation = .landscapeRight + camera.deviceOrientation = .landscapeRight + + XCTAssertEqual(photoSetVideoOrientationCallCount, 1) + XCTAssertEqual(videoSetVideoOrientationCallCount, 1) + } +} diff --git a/camera_avfoundation/darwin/Tests/CameraSetFlashModeTests.swift b/camera_avfoundation/darwin/Tests/CameraSetFlashModeTests.swift new file mode 100644 index 0000000..7a931a7 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraSetFlashModeTests.swift @@ -0,0 +1,168 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import XCTest + +@testable import camera_avfoundation + +final class CameraSetFlashModeTests: XCTestCase { + private func createCamera() -> (Camera, MockCaptureDevice, MockCapturePhotoOutput) { + let mockDevice = MockCaptureDevice() + let mockCapturePhotoOutput = MockCapturePhotoOutput() + + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.videoCaptureDeviceFactory = { _ in mockDevice } + let camera = CameraTestUtils.createTestCamera(configuration) + camera.capturePhotoOutput = mockCapturePhotoOutput + + return (camera, mockDevice, mockCapturePhotoOutput) + } + + func testSetFlashModeWithTorchMode_setsTrochModeOn() { + let (camera, mockDevice, _) = createCamera() + + mockDevice.hasTorch = true + mockDevice.isTorchAvailable = true + + var setTorchModeCalled = false + mockDevice.setTorchModeStub = { torchMode in + XCTAssertEqual(torchMode, .on) + setTorchModeCalled = true + } + + let expectation = expectation(description: "Call completed") + + camera.setFlashMode(.torch) { + result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30) + + XCTAssertTrue(setTorchModeCalled) + } + + func testSetFlashModeWithTorchMode_returnError_ifHasNoTorch() { + let (camera, mockDevice, _) = createCamera() + + mockDevice.hasTorch = false + + let expectation = expectation(description: "Call completed") + + camera.setFlashMode(.torch) { result in + switch result { + case .failure(let error as PigeonError): + XCTAssertEqual(error.code, "setFlashModeFailed") + XCTAssertEqual(error.message, "Device does not support torch mode") + default: + XCTFail("Expected failure") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 30) + } + + func testSetFlashModeWithTorchMode_returnError_ifTorchIsNotAvailable() { + let (camera, mockDevice, _) = createCamera() + + mockDevice.hasTorch = true + mockDevice.isTorchAvailable = false + + let expectation = expectation(description: "Call completed") + + camera.setFlashMode(.torch) { result in + switch result { + case .failure(let error as PigeonError): + XCTAssertEqual(error.code, "setFlashModeFailed") + XCTAssertEqual(error.message, "Torch mode is currently not available") + default: + XCTFail("Expected failure") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 30) + } + + func testSetFlashModeWithNonTorchMode_setsTrochModeOff_ifTorchModeIsEnabled() { + let (camera, mockDevice, mockCapturePhotoOutput) = createCamera() + + mockCapturePhotoOutput.supportedFlashModes = [.auto] + + mockDevice.hasFlash = true + // Torch mode is enabled + mockDevice.getTorchModeStub = { .on } + + var setTorchModeCalled = false + mockDevice.setTorchModeStub = { torchMode in + XCTAssertEqual(torchMode, .off) + setTorchModeCalled = true + } + + let expectation = expectation(description: "Call completed") + + camera.setFlashMode(.auto) { + result in + switch result { + case .success: + break + case .failure: + XCTFail("Unexpected failure") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 30) + + XCTAssertTrue(setTorchModeCalled) + } + + func testSetFlashModeWithNonTorchMode_returnError_ifHasNoFlash() { + let (camera, mockDevice, _) = createCamera() + + mockDevice.hasFlash = false + + let expectation = expectation(description: "Call completed") + + camera.setFlashMode(.auto) { result in + switch result { + case .failure(let error as PigeonError): + XCTAssertEqual(error.code, "setFlashModeFailed") + XCTAssertEqual(error.message, "Device does not have flash capabilities") + default: + XCTFail("Expected failure") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 30) + } + + func testSetFlashModeWithNonTorchMode_returnError_ifModeIsNotSupported() { + let (camera, mockDevice, mockCapturePhotoOutput) = createCamera() + + // No flash modes are supported + mockCapturePhotoOutput.supportedFlashModes = [] + + mockDevice.hasFlash = true + + let expectation = expectation(description: "Call completed") + + camera.setFlashMode(.auto) { result in + switch result { + case .failure(let error as PigeonError): + XCTAssertEqual(error.code, "setFlashModeFailed") + XCTAssertEqual(error.message, "Device does not support this specific flash mode") + default: + XCTFail("Expected failure") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 30) + } +} diff --git a/camera_avfoundation/darwin/Tests/CameraSetFocusModeTests.swift b/camera_avfoundation/darwin/Tests/CameraSetFocusModeTests.swift new file mode 100644 index 0000000..75c1cf3 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraSetFocusModeTests.swift @@ -0,0 +1,166 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import XCTest + +@testable import camera_avfoundation + +final class CameraSetFocusModeTests: XCTestCase { + private func createCamera() -> (Camera, MockCaptureDevice, MockDeviceOrientationProvider) { + let mockDevice = MockCaptureDevice() + let mockDeviceOrientationProvider = MockDeviceOrientationProvider() + + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.videoCaptureDeviceFactory = { _ in mockDevice } + configuration.deviceOrientationProvider = mockDeviceOrientationProvider + let camera = CameraTestUtils.createTestCamera(configuration) + + return (camera, mockDevice, mockDeviceOrientationProvider) + } + + func testAutoFocusWithContinuousModeSupported_ShouldSetContinuousAutoFocus() { + let (camera, mockDevice, _) = createCamera() + // AVCaptureFocusModeContinuousAutoFocus and AVCaptureFocusModeAutoFocus are supported. + mockDevice.isFocusModeSupportedStub = { mode in + mode == .continuousAutoFocus || mode == .autoFocus + } + + var setFocusModeContinuousAutoFocusCalled = false + + mockDevice.setFocusModeStub = { mode in + // Don't expect setFocusMode:AVCaptureFocusModeAutoFocus. + if mode == .autoFocus { + XCTFail("Unexpected call to setFocusMode") + } else if mode == .continuousAutoFocus { + setFocusModeContinuousAutoFocusCalled = true + } + } + + camera.setFocusMode(.auto) + + XCTAssertTrue(setFocusModeContinuousAutoFocusCalled) + } + + func testAutoFocusWithContinuousModeNotSupported_ShouldSetAutoFocus() { + let (camera, mockDevice, _) = createCamera() + // AVCaptureFocusModeContinuousAutoFocus is not supported. + // AVCaptureFocusModeAutoFocus is supported. + mockDevice.isFocusModeSupportedStub = { mode in + mode == .autoFocus + } + + var setFocusModeAutoFocusCalled = false + + // Don't expect setFocusMode:AVCaptureFocusModeContinuousAutoFocus. + mockDevice.setFocusModeStub = { mode in + if mode == .continuousAutoFocus { + XCTFail("Unexpected call to setFocusMode") + } else if mode == .autoFocus { + setFocusModeAutoFocusCalled = true + } + } + + camera.setFocusMode(.auto) + + XCTAssertTrue(setFocusModeAutoFocusCalled) + } + + func testAutoFocusWithNoModeSupported_ShouldSetNothing() { + let (camera, mockDevice, _) = createCamera() + // No modes are supported. + mockDevice.isFocusModeSupportedStub = { _ in + false + } + + // Don't expect any setFocus. + mockDevice.setFocusModeStub = { + _ in XCTFail("Unexpected call to setFocusMode") + } + + camera.setFocusMode(.auto) + } + + func testLockedFocusWithModeSupported_ShouldSetModeAutoFocus() { + let (camera, mockDevice, _) = createCamera() + // AVCaptureFocusModeContinuousAutoFocus and AVCaptureFocusModeAutoFocus are supported. + mockDevice.isFocusModeSupportedStub = { mode in + mode == .continuousAutoFocus || mode == .autoFocus + } + + var setFocusModeAutoFocusCalled = false + + // AVCaptureFocusModeAutoFocus automatically adjusts the focus one time, and then locks focus + mockDevice.setFocusModeStub = { mode in + if mode == .continuousAutoFocus { + XCTFail("Unexpected call to setFocusMode") + } else if mode == .autoFocus { + setFocusModeAutoFocusCalled = true + } + } + + camera.setFocusMode(.locked) + + XCTAssertTrue(setFocusModeAutoFocusCalled) + } + + func testLockedFocusWithModeNotSupported_ShouldSetNothing() { + let (camera, mockDevice, _) = createCamera() + mockDevice.isFocusModeSupportedStub = { mode in + mode == .continuousAutoFocus + } + + // Don't expect any setFocus. + mockDevice.setFocusModeStub = { _ in + XCTFail("Unexpected call to setFocusMode") + } + + camera.setFocusMode(.locked) + } + + func testSetFocusPointWithResult_SetsFocusPointOfInterest() { + let (camera, mockDevice, mockDeviceOrientationProvider) = createCamera() + // UI is currently in landscape left orientation. + mockDeviceOrientationProvider.orientationStub = { .landscapeLeft } + // Focus point of interest is supported. + mockDevice.isFocusPointOfInterestSupported = true + + var setFocusPointOfInterestCalled = false + mockDevice.setFocusPointOfInterestStub = { point in + if point == CGPoint(x: 1.0, y: 1.0) { + setFocusPointOfInterestCalled = true + } + } + + camera.setFocusPoint(PlatformPoint(x: 1, y: 1)) { + result in + let _ = self.assertSuccess(result) + } + + XCTAssertTrue(setFocusPointOfInterestCalled) + } + + func testSetFocusPoint_WhenNotSupported_ReturnsError() { + let (camera, mockDevice, mockDeviceOrientationProvider) = createCamera() + // UI is currently in landscape left orientation. + mockDeviceOrientationProvider.orientationStub = { .landscapeLeft } + // Focus point of interest is not supported. + mockDevice.isFocusPointOfInterestSupported = false + + let expectation = self.expectation(description: "Completion with error") + + camera.setFocusPoint(PlatformPoint(x: 1, y: 1)) { result in + switch result { + case .failure(let error as PigeonError): + XCTAssertEqual(error.code, "setFocusPointFailed") + XCTAssertEqual(error.message, "Device does not have focus point capabilities") + default: + XCTFail("Expected failure") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } +} diff --git a/camera_avfoundation/darwin/Tests/CameraSettingsTests.swift b/camera_avfoundation/darwin/Tests/CameraSettingsTests.swift new file mode 100644 index 0000000..0c9f45e --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraSettingsTests.swift @@ -0,0 +1,356 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import Flutter +import XCTest + +@testable import camera_avfoundation + +private let testResolutionPreset = PlatformResolutionPreset.medium +private let testFramesPerSecond: Int64 = 15 +private let testVideoBitrate: Int64 = 200000 +private let testAudioBitrate: Int64 = 32000 + +private final class TestMediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper { + let lockExpectation: XCTestExpectation + let unlockExpectation: XCTestExpectation + let minFrameDurationExpectation: XCTestExpectation + let maxFrameDurationExpectation: XCTestExpectation + let beginConfigurationExpectation: XCTestExpectation + let commitConfigurationExpectation: XCTestExpectation + let audioSettingsExpectation: XCTestExpectation + let videoSettingsExpectation: XCTestExpectation + + init(test: XCTestCase, expectAudio: Bool) { + lockExpectation = test.expectation(description: "lockExpectation") + unlockExpectation = test.expectation(description: "unlockExpectation") + minFrameDurationExpectation = test.expectation(description: "minFrameDurationExpectation") + maxFrameDurationExpectation = test.expectation(description: "maxFrameDurationExpectation") + beginConfigurationExpectation = test.expectation(description: "beginConfigurationExpectation") + commitConfigurationExpectation = test.expectation(description: "commitConfigurationExpectation") + audioSettingsExpectation = test.expectation(description: "audioSettingsExpectation") + audioSettingsExpectation.isInverted = !expectAudio + videoSettingsExpectation = test.expectation(description: "videoSettingsExpectation") + } + + override func lockDevice(_ captureDevice: CaptureDevice) throws { + lockExpectation.fulfill() + } + + override func unlockDevice(_ captureDevice: CaptureDevice) { + unlockExpectation.fulfill() + } + + override func beginConfiguration(for videoCaptureSession: CaptureSession) { + beginConfigurationExpectation.fulfill() + } + + override func commitConfiguration(for videoCaptureSession: CaptureSession) { + commitConfigurationExpectation.fulfill() + } + + override func setMinFrameDuration(_ duration: CMTime, on captureDevice: CaptureDevice) { + // Camera allows to set frame rate with 1/10 precision. + let expectedDuration = CMTimeMake(value: 10, timescale: Int32(testFramesPerSecond * 10)) + if duration == expectedDuration { + minFrameDurationExpectation.fulfill() + } + } + + override func setMaxFrameDuration(_ duration: CMTime, on captureDevice: CaptureDevice) { + // Camera allows to set frame rate with 1/10 precision. + let expectedDuration = CMTimeMake(value: 10, timescale: Int32(testFramesPerSecond * 10)) + if duration == expectedDuration { + maxFrameDurationExpectation.fulfill() + } + } + + override func assetWriterAudioInput(withOutputSettings outputSettings: [String: Any]?) + -> AssetWriterInput + { + if let bitrate = outputSettings?[AVEncoderBitRateKey] as? Int, bitrate == Int(testAudioBitrate) + { + audioSettingsExpectation.fulfill() + } + return MockAssetWriterInput() + } + + override func assetWriterVideoInput(withOutputSettings outputSettings: [String: Any]?) + -> AssetWriterInput + { + if let compressionProperties = outputSettings?[AVVideoCompressionPropertiesKey] + as? [String: Any], + let bitrate = compressionProperties[AVVideoAverageBitRateKey] as? Int, + let frameRate = compressionProperties[AVVideoExpectedSourceFrameRateKey] as? Double, + bitrate == testVideoBitrate, frameRate == Double(testFramesPerSecond) + { + videoSettingsExpectation.fulfill() + } + + // AVAssetWriterInput needs these three keys, otherwise it throws. + var outputSettingsWithRequiredKeys = outputSettings ?? [:] + outputSettingsWithRequiredKeys[AVVideoCodecKey] = AVVideoCodecType.h264 + outputSettingsWithRequiredKeys[AVVideoWidthKey] = 1280 + outputSettingsWithRequiredKeys[AVVideoHeightKey] = 720 + + return MockAssetWriterInput() + } + + override func addInput(_ writerInput: AssetWriterInput, to writer: AssetWriter) { + // No-op. + } + + override func recommendedVideoSettingsForAssetWriter( + withFileType fileType: AVFileType, for output: CaptureVideoDataOutput + ) -> [String: Any]? { + return [:] + } +} + +final class CameraSettingsTests: XCTestCase { + func testSettings_shouldPassConfigurationToCameraDeviceAndWriter() { + let enableAudio: Bool = true + let settings = PlatformMediaSettings( + resolutionPreset: testResolutionPreset, + framesPerSecond: testFramesPerSecond, + videoBitrate: testVideoBitrate, + audioBitrate: testAudioBitrate, + enableAudio: enableAudio + ) + let injectedWrapper = TestMediaSettingsAVWrapper(test: self, expectAudio: enableAudio) + + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.mediaSettingsWrapper = injectedWrapper + configuration.mediaSettings = settings + let camera = CameraTestUtils.createTestCamera(configuration) + + // Expect FPS configuration is passed to camera device. + wait( + for: [ + injectedWrapper.lockExpectation, + injectedWrapper.beginConfigurationExpectation, + injectedWrapper.minFrameDurationExpectation, + injectedWrapper.maxFrameDurationExpectation, + injectedWrapper.commitConfigurationExpectation, + injectedWrapper.unlockExpectation, + ], timeout: 1, enforceOrder: true) + + camera.startVideoRecording( + completion: { error in + // No-op. + }, messengerForStreaming: nil) + + wait( + for: [ + injectedWrapper.audioSettingsExpectation, + injectedWrapper.videoSettingsExpectation, + ], timeout: 1) + } + + func testSettings_ShouldBeSupportedByMethodCall() { + let mockDevice = MockCaptureDevice() + let mockSession = MockCaptureSession() + mockSession.canSetSessionPresetStub = { _ in true } + let camera = CameraPlugin( + registry: MockFlutterTextureRegistry(), + messenger: MockFlutterBinaryMessenger(), + globalAPI: MockGlobalEventApi(), + deviceDiscoverer: MockCameraDeviceDiscoverer(), + permissionManager: MockCameraPermissionManager(), + deviceFactory: { _ in mockDevice }, + captureSessionFactory: { mockSession }, + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue") + ) + + let expectation = self.expectation(description: "Result finished") + let mediaSettings = PlatformMediaSettings( + resolutionPreset: testResolutionPreset, + framesPerSecond: testFramesPerSecond, + videoBitrate: testVideoBitrate, + audioBitrate: testAudioBitrate, + enableAudio: false + ) + var resultValue: Int64? + camera.createCameraOnSessionQueue( + withName: "acamera", + settings: mediaSettings + ) { result in + resultValue = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + XCTAssertNotNil(resultValue) + } + + func testSettings_ShouldSelectFormatWhichSupports60FPS() { + let settings = PlatformMediaSettings( + resolutionPreset: testResolutionPreset, + framesPerSecond: 60, + videoBitrate: testVideoBitrate, + audioBitrate: testAudioBitrate, + enableAudio: false + ) + + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.mediaSettings = settings + let camera = CameraTestUtils.createTestCamera(configuration) + + let range = camera.captureDevice.flutterActiveFormat.flutterVideoSupportedFrameRateRanges[0] + XCTAssertLessThanOrEqual(range.minFrameRate, 60) + XCTAssertGreaterThanOrEqual(range.maxFrameRate, 60) + } + + func test_setUpCaptureSessionForAudioIfNeeded_skipsAudioSession_whenAudioDisabled() { + let settings = PlatformMediaSettings( + resolutionPreset: testResolutionPreset, + framesPerSecond: testFramesPerSecond, + videoBitrate: testVideoBitrate, + audioBitrate: testAudioBitrate, + enableAudio: false + ) + + let wrapper = TestMediaSettingsAVWrapper(test: self, expectAudio: false) + let mockAudioSession = MockCaptureSession() + + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.mediaSettingsWrapper = wrapper + configuration.mediaSettings = settings + configuration.audioCaptureSession = mockAudioSession + let camera = CameraTestUtils.createTestCamera(configuration) + + wait( + for: [ + wrapper.lockExpectation, + wrapper.beginConfigurationExpectation, + wrapper.minFrameDurationExpectation, + wrapper.maxFrameDurationExpectation, + wrapper.commitConfigurationExpectation, + wrapper.unlockExpectation, + ], + timeout: 1, + enforceOrder: true + ) + + camera.startVideoRecording(completion: { _ in }, messengerForStreaming: nil) + + wait( + for: [ + wrapper.audioSettingsExpectation, + wrapper.videoSettingsExpectation, + ], + timeout: 1 + ) + + XCTAssertEqual( + mockAudioSession.addedAudioOutputCount, 0, + "Audio session should not receive AVCaptureAudioDataOutput when enableAudio is false" + ) + } + + func test_setUpCaptureSessionForAudioIfNeeded_addsAudioSession_whenAudioEnabled() { + let settings = PlatformMediaSettings( + resolutionPreset: testResolutionPreset, + framesPerSecond: testFramesPerSecond, + videoBitrate: testVideoBitrate, + audioBitrate: testAudioBitrate, + enableAudio: true + ) + + let wrapper = TestMediaSettingsAVWrapper(test: self, expectAudio: true) + let mockAudioSession = MockCaptureSession() + + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.mediaSettingsWrapper = wrapper + configuration.mediaSettings = settings + configuration.audioCaptureSession = mockAudioSession + let camera = CameraTestUtils.createTestCamera(configuration) + + wait( + for: [ + wrapper.lockExpectation, + wrapper.beginConfigurationExpectation, + wrapper.minFrameDurationExpectation, + wrapper.maxFrameDurationExpectation, + wrapper.commitConfigurationExpectation, + wrapper.unlockExpectation, + ], + timeout: 1, + enforceOrder: true + ) + + camera.startVideoRecording(completion: { _ in }, messengerForStreaming: nil) + + wait( + for: [ + wrapper.audioSettingsExpectation, + wrapper.videoSettingsExpectation, + ], + timeout: 1 + ) + + XCTAssertGreaterThan( + mockAudioSession.addedAudioOutputCount, 0, + "Audio session should receive AVCaptureAudioDataOutput when enableAudio is true" + ) + } + + func testResolutionPresetWithMax_mustIgnoreLossyFormatsAndSquares() { + let videoSessionMock = MockCaptureSession() + videoSessionMock.canSetSessionPresetStub = { _ in true } + + let lossyFormat = MockCaptureDeviceFormat( + codecType: 1_651_798_066, // 'btp2' + width: 4224, + height: 3024 + ) + let squareFormat = MockCaptureDeviceFormat( + codecType: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + width: 4032, + height: 4032 + ) + let safe4KFormat = MockCaptureDeviceFormat( + codecType: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + width: 3840, + height: 2160 + ) + + let captureDeviceMock = MockCaptureDevice() + captureDeviceMock.flutterFormats = [lossyFormat, squareFormat, safe4KFormat] + + var currentFormat: CaptureDeviceFormat = safe4KFormat + captureDeviceMock.activeFormatStub = { currentFormat } + captureDeviceMock.setActiveFormatStub = { newFormat in currentFormat = newFormat } + + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.videoCaptureDeviceFactory = { _ in captureDeviceMock } + configuration.videoCaptureSession = videoSessionMock + + configuration.videoDimensionsConverter = { format in + return CMVideoFormatDescriptionGetDimensions(format.formatDescription) + } + + configuration.mediaSettings = CameraTestUtils.createDefaultMediaSettings( + resolutionPreset: PlatformResolutionPreset.max + ) + + let _ = CameraTestUtils.createTestCamera(configuration) + + let selectedFormat = captureDeviceMock.flutterActiveFormat + let selectedDimensions = CMVideoFormatDescriptionGetDimensions(selectedFormat.formatDescription) + + XCTAssertEqual( + selectedDimensions.width, + 3840, + "Camera should have ignored the lossy and square formats, safely falling back to 4K." + ) + XCTAssertEqual( + selectedDimensions.height, + 2160, + "Camera should have ignored the lossy and square formats, safely falling back to 4K." + ) + } +} diff --git a/camera_avfoundation/darwin/Tests/CameraTestUtils.swift b/camera_avfoundation/darwin/Tests/CameraTestUtils.swift new file mode 100644 index 0000000..a46d23c --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraTestUtils.swift @@ -0,0 +1,219 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import XCTest + +@testable import camera_avfoundation + +/// Utils for creating default class instances used in tests +enum CameraTestUtils { + /// This method provides a convenient way to create media settings with minimal configuration. + /// Audio is enabled by default, while other parameters use platform-specific defaults. + static func createDefaultMediaSettings(resolutionPreset: PlatformResolutionPreset) + -> PlatformMediaSettings + { + return PlatformMediaSettings( + resolutionPreset: resolutionPreset, + framesPerSecond: nil, + videoBitrate: nil, + audioBitrate: nil, + enableAudio: true) + } + + /// Creates a test `CameraConfiguration` with a default mock setup. + static func createTestCameraConfiguration() -> CameraConfiguration { + let captureSessionQueue = DispatchQueue(label: "capture_session_queue") + + let videoSessionMock = MockCaptureSession() + videoSessionMock.canSetSessionPresetStub = { _ in true } + + let audioSessionMock = MockCaptureSession() + audioSessionMock.canSetSessionPresetStub = { _ in true } + + let frameRateRangeMock1 = MockFrameRateRange.init(minFrameRate: 3, maxFrameRate: 30) + + let captureDeviceFormatMock1 = MockCaptureDeviceFormat() + captureDeviceFormatMock1.flutterVideoSupportedFrameRateRanges = [frameRateRangeMock1] + + let frameRateRangeMock2 = MockFrameRateRange.init(minFrameRate: 3, maxFrameRate: 60) + + let captureDeviceFormatMock2 = MockCaptureDeviceFormat() + captureDeviceFormatMock2.flutterVideoSupportedFrameRateRanges = [frameRateRangeMock2] + + let captureDeviceMock = MockCaptureDevice() + captureDeviceMock.flutterFormats = [captureDeviceFormatMock1, captureDeviceFormatMock2] + + var currentFormat: CaptureDeviceFormat = captureDeviceFormatMock1 + + captureDeviceMock.activeFormatStub = { currentFormat } + captureDeviceMock.setActiveFormatStub = { format in + currentFormat = format + } + + let configuration = CameraConfiguration( + mediaSettings: createDefaultMediaSettings( + resolutionPreset: PlatformResolutionPreset.medium), + mediaSettingsWrapper: FLTCamMediaSettingsAVWrapper(), + captureDeviceFactory: { _ in captureDeviceMock }, + audioCaptureDeviceFactory: { MockCaptureDevice() }, + captureSessionFactory: { videoSessionMock }, + captureSessionQueue: captureSessionQueue, + captureDeviceInputFactory: MockCaptureDeviceInputFactory(), + initialCameraName: "camera_name" + ) + + configuration.videoCaptureSession = videoSessionMock + configuration.audioCaptureSession = audioSessionMock + configuration.orientation = .portrait + + configuration.assetWriterFactory = { _, _ in MockAssetWriter() } + + configuration.inputPixelBufferAdaptorFactory = { _, _ in + MockAssetWriterInputPixelBufferAdaptor() + } + + return configuration + } + + static func createTestCamera(_ configuration: CameraConfiguration) -> DefaultCamera { + return (try? DefaultCamera(configuration: configuration))! + } + + static func createTestCamera() -> DefaultCamera { + return createTestCamera(createTestCameraConfiguration()) + } + + static func createCameraWithCaptureSessionQueue(_ captureSessionQueue: DispatchQueue) + -> DefaultCamera + { + let configuration = createTestCameraConfiguration() + configuration.captureSessionQueue = captureSessionQueue + return createTestCamera(configuration) + } + + /// Creates a test sample buffer. + /// @return a test sample buffer. + static func createTestSampleBuffer( + timestamp: CMTime = .zero, + duration: CMTime = CMTimeMake(value: 1, timescale: 44100) + ) -> CMSampleBuffer { + var pixelBuffer: CVPixelBuffer? + CVPixelBufferCreate(kCFAllocatorDefault, 100, 100, kCVPixelFormatType_32BGRA, nil, &pixelBuffer) + + var formatDescription: CMFormatDescription? + CMVideoFormatDescriptionCreateForImageBuffer( + allocator: kCFAllocatorDefault, + imageBuffer: pixelBuffer!, + formatDescriptionOut: &formatDescription) + + var timingInfo = CMSampleTimingInfo( + duration: duration, + presentationTimeStamp: timestamp, + decodeTimeStamp: .invalid) + + var sampleBuffer: CMSampleBuffer? + CMSampleBufferCreateReadyWithImageBuffer( + allocator: kCFAllocatorDefault, + imageBuffer: pixelBuffer!, + formatDescription: formatDescription!, + sampleTiming: &timingInfo, + sampleBufferOut: &sampleBuffer) + + return sampleBuffer! + } + + /// Creates a test audio sample buffer. + /// @return a test audio sample buffer. + static func createTestAudioSampleBuffer( + timestamp: CMTime = .zero, + duration: CMTime = CMTimeMake(value: 1, timescale: 44100) + ) -> CMSampleBuffer { + var blockBuffer: CMBlockBuffer? + CMBlockBufferCreateWithMemoryBlock( + allocator: kCFAllocatorDefault, + memoryBlock: nil, + blockLength: Int(duration.value), + blockAllocator: kCFAllocatorDefault, + customBlockSource: nil, + offsetToData: 0, + dataLength: Int(duration.value), + flags: kCMBlockBufferAssureMemoryNowFlag, + blockBufferOut: &blockBuffer) + + var formatDescription: CMFormatDescription? + var basicDescription = AudioStreamBasicDescription( + mSampleRate: Float64(duration.timescale), + mFormatID: kAudioFormatLinearPCM, + mFormatFlags: 0, + mBytesPerPacket: 1, + mFramesPerPacket: 1, + mBytesPerFrame: 1, + mChannelsPerFrame: 1, + mBitsPerChannel: 8, + mReserved: 0) + + CMAudioFormatDescriptionCreate( + allocator: kCFAllocatorDefault, + asbd: &basicDescription, + layoutSize: 0, + layout: nil, + magicCookieSize: 0, + magicCookie: nil, + extensions: nil, + formatDescriptionOut: &formatDescription) + + var sampleBuffer: CMSampleBuffer? + CMAudioSampleBufferCreateReadyWithPacketDescriptions( + allocator: kCFAllocatorDefault, + dataBuffer: blockBuffer!, + formatDescription: formatDescription!, + sampleCount: CMItemCount(duration.value), + presentationTimeStamp: timestamp, + packetDescriptions: nil, + sampleBufferOut: &sampleBuffer) + + return sampleBuffer! + } + + static func createTestAudioOutput() -> AVCaptureOutput { + return AVCaptureAudioDataOutput() + } + + static func createTestConnection(_ output: AVCaptureOutput) -> AVCaptureConnection { + return AVCaptureConnection(inputPorts: [], output: output) + } +} + +extension XCTestCase { + /// Wait until a round trip of a given `DispatchQueue` is complete. This allows for testing + /// side-effects of async functions that do not provide any notification of completion. + func waitForQueueRoundTrip(with queue: DispatchQueue) { + let expectation = expectation(description: "Queue flush") + + queue.async { + if queue == DispatchQueue.main { + expectation.fulfill() + } else { + DispatchQueue.main.async { + expectation.fulfill() + } + } + } + + wait(for: [expectation], timeout: 1) + } + + func assertSuccess( + _ result: Result, file: StaticString = #file, line: UInt = #line + ) -> T? { + switch result { + case .success(let value): + return value + case .failure(let error): + XCTFail("Expected success, but got failure: \(error)", file: file, line: line) + return nil + } + } +} diff --git a/camera_avfoundation/darwin/Tests/CameraZoomTests.swift b/camera_avfoundation/darwin/Tests/CameraZoomTests.swift new file mode 100644 index 0000000..f0c62ad --- /dev/null +++ b/camera_avfoundation/darwin/Tests/CameraZoomTests.swift @@ -0,0 +1,111 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import XCTest + +@testable import camera_avfoundation + +final class CameraZoomTests: XCTestCase { + private func createCamera() -> (Camera, MockCaptureDevice) { + let mockDevice = MockCaptureDevice() + + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.videoCaptureDeviceFactory = { _ in mockDevice } + let camera = CameraTestUtils.createTestCamera(configuration) + + return (camera, mockDevice) + } + + func testSetZoomLevel_setVideoZoomFactor() { + let (camera, mockDevice) = createCamera() + + mockDevice.maxAvailableVideoZoomFactor = 2.0 + mockDevice.minAvailableVideoZoomFactor = 0.0 + + let targetZoom = CGFloat(1.0) + + var setVideoZoomFactorCalled = false + mockDevice.setVideoZoomFactorStub = { zoom in + XCTAssertEqual(zoom, targetZoom) + setVideoZoomFactorCalled = true + } + + let expectation = expectation(description: "Call completed") + + camera.setZoomLevel(targetZoom) { + result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30) + + XCTAssertTrue(setVideoZoomFactorCalled) + } + + func testSetZoomLevel_returnsError_forZoomLevelBlowMinimum() { + let (camera, mockDevice) = createCamera() + + // Allowed zoom range between 2.0 and 3.0 + mockDevice.maxAvailableVideoZoomFactor = 2.0 + mockDevice.minAvailableVideoZoomFactor = 3.0 + + let expectation = expectation(description: "Call completed") + + camera.setZoomLevel(CGFloat(1.0)) { result in + switch result { + case .failure(let error as PigeonError): + XCTAssertEqual(error.code, "ZOOM_ERROR") + default: + XCTFail("Expected failure") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 30) + } + + func testSetZoomLevel_returnsError_forZoomLevelAboveMaximum() { + let (camera, mockDevice) = createCamera() + + // Allowed zoom range between 0.0 and 1.0 + mockDevice.maxAvailableVideoZoomFactor = 0.0 + mockDevice.minAvailableVideoZoomFactor = 1.0 + + let expectation = expectation(description: "Call completed") + + camera.setZoomLevel(CGFloat(2.0)) { result in + switch result { + case .failure(let error as PigeonError): + XCTAssertEqual(error.code, "ZOOM_ERROR") + default: + XCTFail("Expected failure") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 30) + } + + func testMaximumAvailableZoomFactor_returnsDeviceMaxAvailableVideoZoomFactor() { + let (camera, mockDevice) = createCamera() + + let targetZoom = CGFloat(1.0) + + mockDevice.maxAvailableVideoZoomFactor = CGFloat(targetZoom) + + XCTAssertEqual(camera.maximumAvailableZoomFactor, targetZoom) + } + + func testMinimumAvailableZoomFactor_returnsDeviceMinAvailableVideoZoomFactor() { + let (camera, mockDevice) = createCamera() + + let targetZoom = CGFloat(1.0) + + mockDevice.minAvailableVideoZoomFactor = CGFloat(targetZoom) + + XCTAssertEqual(camera.minimumAvailableZoomFactor, targetZoom) + } +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockAssetWriter.swift b/camera_avfoundation/darwin/Tests/Mocks/MockAssetWriter.swift new file mode 100644 index 0000000..2b345c0 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockAssetWriter.swift @@ -0,0 +1,33 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +@testable import camera_avfoundation + +/// Mock implementation of `AssetWriter` protocol which allows injecting a custom +/// implementation. +final class MockAssetWriter: NSObject, AssetWriter { + var statusStub: (() -> AVAssetWriter.Status)? + var startWritingStub: (() -> Bool)? + var finishWritingStub: ((() -> Void) -> Void)? + + var status: AVAssetWriter.Status { + return statusStub?() ?? .unknown + } + + var error: Error? = nil + + func startWriting() -> Bool { + return startWritingStub?() ?? true + } + + func finishWriting(completionHandler handler: @escaping () -> Void) { + finishWritingStub?(handler) + } + + func startSession(atSourceTime startTime: CMTime) {} + + func add(_ input: AVAssetWriterInput) {} +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockAssetWriterInput.swift b/camera_avfoundation/darwin/Tests/Mocks/MockAssetWriterInput.swift new file mode 100644 index 0000000..9e70646 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockAssetWriterInput.swift @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +@testable import camera_avfoundation + +/// Mock implementation of `AssetWriterInput` protocol which allows injecting a custom +/// implementation. +final class MockAssetWriterInput: NSObject, AssetWriterInput { + var appendStub: ((CMSampleBuffer) -> Bool)? + + var avInput: AVAssetWriterInput { + preconditionFailure("Attempted to access unimplemented property: avInput") + } + + var expectsMediaDataInRealTime = false + + var isReadyForMoreMediaData = false + + func append(_ sampleBuffer: CMSampleBuffer) -> Bool { + return appendStub?(sampleBuffer) ?? false + } +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockAssetWriterInputPixelBufferAdaptor.swift b/camera_avfoundation/darwin/Tests/Mocks/MockAssetWriterInputPixelBufferAdaptor.swift new file mode 100644 index 0000000..a27106e --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockAssetWriterInputPixelBufferAdaptor.swift @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +@testable import camera_avfoundation + +/// Mock implementation of `AssetWriterInputPixelBufferAdaptor` protocol which allows injecting a custom +/// implementation. +final class MockAssetWriterInputPixelBufferAdaptor: NSObject, AssetWriterInputPixelBufferAdaptor { + var appendStub: ((CVPixelBuffer, CMTime) -> Bool)? + + func append(_ pixelBuffer: CVPixelBuffer, withPresentationTime presentationTime: CMTime) -> Bool { + appendStub?(pixelBuffer, presentationTime) ?? true + } +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockCamera.swift b/camera_avfoundation/darwin/Tests/Mocks/MockCamera.swift new file mode 100644 index 0000000..569f556 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockCamera.swift @@ -0,0 +1,238 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import Flutter + +@testable import camera_avfoundation + +final class MockCamera: NSObject, Camera { + var setDartApiStub: ((CameraEventApi?) -> Void)? + var setOnFrameAvailableStub: (((() -> Void)?) -> Void)? + var getMinimumExposureOffsetStub: (() -> CGFloat)? + var getMaximumExposureOffsetStub: (() -> CGFloat)? + var getMinimumAvailableZoomFactorStub: (() -> CGFloat)? + var getMaximumAvailableZoomFactorStub: (() -> CGFloat)? + var setUpCaptureSessionForAudioIfNeededStub: (() -> Void)? + var receivedImageStreamDataStub: (() -> Void)? + var startStub: (() -> Void)? + var startVideoRecordingStub: + ((@escaping (Result) -> Void, FlutterBinaryMessenger?) -> Void)? + var pauseVideoRecordingStub: (() -> Void)? + var resumeVideoRecordingStub: (() -> Void)? + var stopVideoRecordingStub: ((@escaping (Result) -> Void) -> Void)? + var captureToFileStub: ((@escaping (Result) -> Void) -> Void)? + var setDeviceOrientationStub: ((UIDeviceOrientation) -> Void)? + var lockCaptureOrientationStub: ((PlatformDeviceOrientation) -> Void)? + var unlockCaptureOrientationStub: (() -> Void)? + var setImageFileFormatStub: ((PlatformImageFileFormat) -> Void)? + var setJpegImageQualityStub: ((Int64) -> Void)? + var setExposureModeStub: ((PlatformExposureMode) -> Void)? + var setExposureOffsetStub: ((Double) -> Void)? + var setExposurePointStub: ((PlatformPoint?, @escaping (Result) -> Void) -> Void)? + var setFocusModeStub: ((PlatformFocusMode) -> Void)? + var setFocusPointStub: ((PlatformPoint?, @escaping (Result) -> Void) -> Void)? + var setZoomLevelStub: ((CGFloat, @escaping (Result) -> Void) -> Void)? + var setFlashModeStub: ((PlatformFlashMode, @escaping (Result) -> Void) -> Void)? + var pausePreviewStub: (() -> Void)? + var resumePreviewStub: (() -> Void)? + var setDescriptionWhileRecordingStub: + ((String, @escaping (Result) -> Void) -> Void)? + var startImageStreamStub: + ((FlutterBinaryMessenger, @escaping (Result) -> Void) -> Void)? + var stopImageStreamStub: (() -> Void)? + var setVideoStabilizationModeStub: + ((PlatformVideoStabilizationMode, @escaping (Result) -> Void) -> Void)? + var getIsVideoStabilizationModeSupportedStub: ((PlatformVideoStabilizationMode) -> Bool)? + + var dartAPI: CameraEventApi? { + get { + preconditionFailure("Attempted to access unimplemented property: dartAPI") + } + set { + setDartApiStub?(newValue) + } + } + + var onFrameAvailable: (() -> Void)? { + get { + preconditionFailure("Attempted to access unimplemented property: onFrameAvailable") + } + set { + setOnFrameAvailableStub?(newValue) + } + } + + var videoFormat: FourCharCode = kCVPixelFormatType_32BGRA + + var isPreviewPaused: Bool = false + var isStreamingImages: Bool = false + + var deviceOrientation: UIDeviceOrientation { + get { + preconditionFailure("Attempted to access unimplemented property: deviceOrientation") + } + set { + setDeviceOrientationStub?(newValue) + } + } + + var minimumExposureOffset: CGFloat { + return getMinimumExposureOffsetStub?() ?? 0 + } + + var maximumExposureOffset: CGFloat { + return getMaximumExposureOffsetStub?() ?? 0 + } + + var minimumAvailableZoomFactor: CGFloat { + return getMinimumAvailableZoomFactorStub?() ?? 0 + } + + var maximumAvailableZoomFactor: CGFloat { + return getMaximumAvailableZoomFactorStub?() ?? 0 + } + + func setUpCaptureSessionForAudioIfNeeded() { + setUpCaptureSessionForAudioIfNeededStub?() + } + + func reportInitializationState() {} + + func receivedImageStreamData() { + receivedImageStreamDataStub?() + } + + func start() { + startStub?() + } + + func stop() {} + + func startVideoRecording( + completion: @escaping (Result) -> Void, + messengerForStreaming messenger: FlutterBinaryMessenger? + ) { + startVideoRecordingStub?(completion, messenger) + } + + func pauseVideoRecording() { + pauseVideoRecordingStub?() + } + + func resumeVideoRecording() { + resumeVideoRecordingStub?() + } + + func stopVideoRecording(completion: @escaping (Result) -> Void) { + stopVideoRecordingStub?(completion) + } + + func captureToFile(completion: @escaping (Result) -> Void) { + captureToFileStub?(completion) + } + + func lockCaptureOrientation(_ orientation: PlatformDeviceOrientation) { + lockCaptureOrientationStub?(orientation) + } + + func unlockCaptureOrientation() { + unlockCaptureOrientationStub?() + } + + func setImageFileFormat(_ fileFormat: PlatformImageFileFormat) { + setImageFileFormatStub?(fileFormat) + } + + func setJpegImageQuality(_ quality: Int64) { + setJpegImageQualityStub?(quality) + } + + func setExposureMode(_ mode: PlatformExposureMode) { + setExposureModeStub?(mode) + } + + func setExposureOffset(_ offset: Double) { + setExposureOffsetStub?(offset) + } + + func setExposurePoint( + _ point: PlatformPoint?, withCompletion: @escaping (Result) -> Void + ) { + setExposurePointStub?(point, withCompletion) + } + + func setFocusMode(_ mode: PlatformFocusMode) { + setFocusModeStub?(mode) + } + + func setFocusPoint( + _ point: PlatformPoint?, completion: @escaping (Result) -> Void + ) { + setFocusPointStub?(point, completion) + } + + func setZoomLevel( + _ zoom: CGFloat, + withCompletion completion: @escaping (Result) -> Void + ) { + setZoomLevelStub?(zoom, completion) + } + + func setFlashMode( + _ mode: PlatformFlashMode, + withCompletion completion: @escaping (Result) -> Void + ) { + setFlashModeStub?(mode, completion) + } + + func pausePreview() { + pausePreviewStub?() + } + + func resumePreview() { + resumePreviewStub?() + } + + func setVideoStabilizationMode( + _ mode: PlatformVideoStabilizationMode, + withCompletion completion: @escaping (Result) -> Void + ) { + setVideoStabilizationModeStub?(mode, completion) + } + + func isVideoStabilizationModeSupported(_ mode: PlatformVideoStabilizationMode) -> Bool { + return getIsVideoStabilizationModeSupportedStub?(mode) ?? false + } + + func setDescriptionWhileRecording( + _ cameraName: String, + withCompletion completion: @escaping (Result) -> Void + ) { + setDescriptionWhileRecordingStub?(cameraName, completion) + } + + func startImageStream( + with messenger: FlutterBinaryMessenger, + completion: @escaping (Result) -> Void + ) { + startImageStreamStub?(messenger, completion) + } + + func stopImageStream() { + stopImageStreamStub?() + } + + func captureOutput( + _ output: AVCaptureOutput, + didOutput sampleBuffer: CMSampleBuffer, + from connection: AVCaptureConnection + ) {} + + func close() {} + + func copyPixelBuffer() -> Unmanaged? { + return nil + } +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockCameraDeviceDiscoverer.swift b/camera_avfoundation/darwin/Tests/Mocks/MockCameraDeviceDiscoverer.swift new file mode 100644 index 0000000..f621aa6 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockCameraDeviceDiscoverer.swift @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +@testable import camera_avfoundation + +/// Mock implementation of `CameraDeviceDiscoverer` protocol which allows injecting a custom +/// implementation for session discovery. +final class MockCameraDeviceDiscoverer: NSObject, CameraDeviceDiscoverer { + var discoverySessionStub: + ( + ( + _ deviceTypes: [AVCaptureDevice.DeviceType], + _ mediaType: AVMediaType, + _ position: AVCaptureDevice.Position + ) -> [NSObject & CaptureDevice]? + )? + + /// A stub that replaces the default implementation of + /// `discoverySessionWithDeviceTypes:mediaType:position`. + func discoverySession( + withDeviceTypes deviceTypes: [AVCaptureDevice.DeviceType], mediaType: AVMediaType, + position: AVCaptureDevice.Position + ) -> [CaptureDevice] { + return discoverySessionStub?(deviceTypes, mediaType, position) ?? [] + } +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockCaptureConnection.swift b/camera_avfoundation/darwin/Tests/Mocks/MockCaptureConnection.swift new file mode 100644 index 0000000..d6ea85f --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockCaptureConnection.swift @@ -0,0 +1,27 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +@testable import camera_avfoundation + +/// A mock implementation of `CaptureConnection` that allows injecting a custom implementation. +final class MockCaptureConnection: NSObject, CaptureConnection { + var setVideoOrientationStub: ((AVCaptureVideoOrientation) -> Void)? + + var connection: AVCaptureConnection { + preconditionFailure("Attempted to access unimplemented property: connection") + } + var isVideoMirrored = false + var videoOrientation: AVCaptureVideoOrientation { + get { AVCaptureVideoOrientation.portrait } + set { + setVideoOrientationStub?(newValue) + } + } + var inputPorts: [AVCaptureInput.Port] = [] + var isVideoMirroringSupported = false + var isVideoOrientationSupported = false + var preferredVideoStabilizationMode = AVCaptureVideoStabilizationMode.off +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockCaptureDevice.swift b/camera_avfoundation/darwin/Tests/Mocks/MockCaptureDevice.swift new file mode 100644 index 0000000..71fe2fc --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockCaptureDevice.swift @@ -0,0 +1,127 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +@testable import camera_avfoundation + +/// A mock implementation of `CaptureDevice` that allows mocking the class +/// properties. +class MockCaptureDevice: NSObject, CaptureDevice { + var activeFormatStub: (() -> CaptureDeviceFormat)? + var setActiveFormatStub: ((CaptureDeviceFormat) -> Void)? + var getTorchModeStub: (() -> AVCaptureDevice.TorchMode)? + var setTorchModeStub: ((AVCaptureDevice.TorchMode) -> Void)? + var isFocusModeSupportedStub: ((AVCaptureDevice.FocusMode) -> Bool)? + var setFocusModeStub: ((AVCaptureDevice.FocusMode) -> Void)? + var setFocusPointOfInterestStub: ((CGPoint) -> Void)? + var setExposureModeStub: ((AVCaptureDevice.ExposureMode) -> Void)? + var setExposurePointOfInterestStub: ((CGPoint) -> Void)? + var setExposureTargetBiasStub: ((Float, ((CMTime) -> Void)?) -> Void)? + var isExposureModeSupportedStub: ((AVCaptureDevice.ExposureMode) -> Bool)? + var setVideoZoomFactorStub: ((CGFloat) -> Void)? + var lockForConfigurationStub: (() throws -> Void)? + + var avDevice: AVCaptureDevice { + preconditionFailure("Attempted to access unimplemented property: device") + } + + var uniqueID = "" + var position = AVCaptureDevice.Position.unspecified + var deviceType = AVCaptureDevice.DeviceType.builtInWideAngleCamera + + var flutterActiveFormat: CaptureDeviceFormat { + get { + activeFormatStub?() ?? MockCaptureDeviceFormat() + } + set { + setActiveFormatStub?(newValue) + } + } + + var flutterFormats: [CaptureDeviceFormat] = [] + var hasFlash = false + var hasTorch = false + var isTorchAvailable = false + var torchMode: AVCaptureDevice.TorchMode { + get { + getTorchModeStub?() ?? .off + } + set { + setTorchModeStub?(newValue) + } + } + var isFocusPointOfInterestSupported = false + var maxAvailableVideoZoomFactor = CGFloat(0) + var minAvailableVideoZoomFactor = CGFloat(0) + var videoZoomFactor: CGFloat { + get { 0 } + set { + setVideoZoomFactorStub?(newValue) + } + } + var isExposurePointOfInterestSupported = false + var minExposureTargetBias = Float(0) + var maxExposureTargetBias = Float(0) + var activeVideoMinFrameDuration = CMTime(value: 1, timescale: 1) + var activeVideoMaxFrameDuration = CMTime(value: 1, timescale: 1) + + func isFlashModeSupported(_ mode: AVCaptureDevice.FlashMode) -> Bool { + return false + } + + func isFocusModeSupported(_ mode: AVCaptureDevice.FocusMode) -> Bool { + return isFocusModeSupportedStub?(mode) ?? false + } + + var focusMode: AVCaptureDevice.FocusMode { + get { .autoFocus } + set { setFocusModeStub?(newValue) } + } + + func setFocusMode(_ focusMode: AVCaptureDevice.FocusMode) { + setFocusModeStub?(focusMode) + } + + var focusPointOfInterest: CGPoint { + get { CGPoint.zero } + set { setFocusPointOfInterestStub?(newValue) } + } + + var exposureMode: AVCaptureDevice.ExposureMode { + get { .autoExpose } + set { setExposureModeStub?(newValue) } + } + + var exposurePointOfInterest: CGPoint { + get { CGPoint.zero } + set { setExposurePointOfInterestStub?(newValue) } + } + + func setExposureTargetBias(_ bias: Float, completionHandler handler: ((CMTime) -> Void)? = nil) { + setExposureTargetBiasStub?(bias, handler) + } + + func isExposureModeSupported(_ mode: AVCaptureDevice.ExposureMode) -> Bool { + return isExposureModeSupportedStub?(mode) ?? false + } + + var lensAperture: Float { 1.8 } + + var exposureDuration: CMTime { CMTime(value: 1, timescale: 1) } + + var iso: Float { 100 } + + func isVideoStabilizationModeSupported(_ videoStabilizationMode: AVCaptureVideoStabilizationMode) + -> Bool + { + return false + } + + func lockForConfiguration() throws { + try lockForConfigurationStub?() + } + + func unlockForConfiguration() {} +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockCaptureDeviceFormat.swift b/camera_avfoundation/darwin/Tests/Mocks/MockCaptureDeviceFormat.swift new file mode 100644 index 0000000..557ff4b --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockCaptureDeviceFormat.swift @@ -0,0 +1,43 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +@testable import camera_avfoundation + +/// A mock implementation of `CaptureDeviceFormat` that allows mocking the class +/// properties. +final class MockCaptureDeviceFormat: NSObject, CaptureDeviceFormat { + + /// The format associated with the capture device. + var avFormat: AVCaptureDevice.Format { + preconditionFailure("Attempted to access unimplemented property: avFormat") + } + + var _formatDescription: CMVideoFormatDescription? + + /// The format description for the capture device. + var formatDescription: CMFormatDescription { + _formatDescription! + } + + /// The array of frame rate ranges supported by the video format. + var flutterVideoSupportedFrameRateRanges: [FrameRateRange] = [] + + override init() { + super.init() + + CMVideoFormatDescriptionCreate( + allocator: kCFAllocatorDefault, codecType: kCVPixelFormatType_32BGRA, width: 1920, + height: 1080, extensions: nil, formatDescriptionOut: &_formatDescription) + } + + init(codecType: OSType, width: Int32, height: Int32) { + super.init() + + CMVideoFormatDescriptionCreate( + allocator: kCFAllocatorDefault, codecType: codecType, width: width, + height: height, extensions: nil, formatDescriptionOut: &_formatDescription) + } +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockCaptureDeviceInputFactory.swift b/camera_avfoundation/darwin/Tests/Mocks/MockCaptureDeviceInputFactory.swift new file mode 100644 index 0000000..0222750 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockCaptureDeviceInputFactory.swift @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Foundation + +@testable import camera_avfoundation + +///// A mocked implementation of CaptureDeviceInputFactory which allows injecting a custom +///// implementation. +final class MockCaptureDeviceInputFactory: NSObject, CaptureDeviceInputFactory { + func deviceInput(with device: CaptureDevice) throws -> CaptureInput { + return MockCaptureInput() + } +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockCaptureInput.swift b/camera_avfoundation/darwin/Tests/Mocks/MockCaptureInput.swift new file mode 100644 index 0000000..394dbf9 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockCaptureInput.swift @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +@testable import camera_avfoundation + +/// A mocked implementation of CaptureInput which allows injecting a custom +/// implementation. +final class MockCaptureInput: NSObject, CaptureInput { + var avInput: AVCaptureInput { + preconditionFailure("Attempted to access unimplemented property: input") + } + + var ports: [AVCaptureInput.Port] = [] +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockCapturePhotoOutput.swift b/camera_avfoundation/darwin/Tests/Mocks/MockCapturePhotoOutput.swift new file mode 100644 index 0000000..184fd0b --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockCapturePhotoOutput.swift @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +@testable import camera_avfoundation + +/// Mock implementation of `CapturePhotoOutput` protocol which allows injecting a custom +/// implementation. +final class MockCapturePhotoOutput: NSObject, CapturePhotoOutput { + var avOutput = AVCapturePhotoOutput() + var availablePhotoCodecTypes: [AVVideoCodecType] = [] + var isHighResolutionCaptureEnabled = false + var supportedFlashModes: [AVCaptureDevice.FlashMode] = [] + + // Stub that is called when the corresponding public method is called. + var capturePhotoWithSettingsStub: + ((_ settings: AVCapturePhotoSettings, _ delegate: AVCapturePhotoCaptureDelegate) -> Void)? + + // Stub that is called when the corresponding public method is called. + var connectionWithMediaTypeStub: ((_ mediaType: AVMediaType) -> CaptureConnection?)? + + func capturePhoto(with settings: AVCapturePhotoSettings, delegate: AVCapturePhotoCaptureDelegate) + { + capturePhotoWithSettingsStub?(settings, delegate) + } + + func connection(with mediaType: AVMediaType) -> CaptureConnection? { + return connectionWithMediaTypeStub?(mediaType) + } +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockCaptureSession.swift b/camera_avfoundation/darwin/Tests/Mocks/MockCaptureSession.swift new file mode 100644 index 0000000..23ce72b --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockCaptureSession.swift @@ -0,0 +1,87 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +@testable import camera_avfoundation + +/// Mock implementation of `CaptureSession` protocol which allows injecting a custom +/// implementation. +final class MockCaptureSession: NSObject, CaptureSession { + var setSessionPresetStub: ((AVCaptureSession.Preset) -> Void)? + var beginConfigurationStub: (() -> Void)? + var commitConfigurationStub: (() -> Void)? + var startRunningStub: (() -> Void)? + var stopRunningStub: (() -> Void)? + var canSetSessionPresetStub: ((AVCaptureSession.Preset) -> Bool)? + + var _sessionPreset = AVCaptureSession.Preset.high + var inputs = [AVCaptureInput]() + var outputs = [AVCaptureOutput]() + + private(set) var addedAudioOutputCount: Int = 0 + + var automaticallyConfiguresApplicationAudioSession = false + var isRunning = true + + var sessionPreset: AVCaptureSession.Preset { + get { + return _sessionPreset + } + set { + setSessionPresetStub?(newValue) + } + } + + func beginConfiguration() { + beginConfigurationStub?() + } + + func commitConfiguration() { + commitConfigurationStub?() + } + + func startRunning() { + startRunningStub?() + } + + func stopRunning() { + stopRunningStub?() + } + + func canSetSessionPreset(_ preset: AVCaptureSession.Preset) -> Bool { + return canSetSessionPresetStub?(preset) ?? true + } + + func addInputWithNoConnections(_ input: CaptureInput) {} + + func addOutputWithNoConnections(_ output: AVCaptureOutput) {} + + func addConnection(_: AVCaptureConnection) {} + + func addInput(_: CaptureInput) {} + + func addOutput(_ output: AVCaptureOutput) { + + if output is AVCaptureAudioDataOutput { + addedAudioOutputCount += 1 + } + } + + func removeInput(_: CaptureInput) {} + + func removeOutput(_: AVCaptureOutput) {} + + func canAddInput(_: CaptureInput) -> Bool { + return true + } + + func canAddOutput(_: AVCaptureOutput) -> Bool { + return true + } + + func canAddConnection(_: AVCaptureConnection) -> Bool { + return true + } +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockCaptureVideoDataOutput.swift b/camera_avfoundation/darwin/Tests/Mocks/MockCaptureVideoDataOutput.swift new file mode 100644 index 0000000..23e5473 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockCaptureVideoDataOutput.swift @@ -0,0 +1,27 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +@testable import camera_avfoundation + +/// Mock implementation of `CaptureVideoDataOutput` protocol which allows injecting a custom +/// implementation. +class MockCaptureVideoDataOutput: NSObject, CaptureVideoDataOutput { + var avOutput = AVCaptureVideoDataOutput() + var alwaysDiscardsLateVideoFrames = false + var videoSettings: [String: Any]! = [:] + var availableVideoPixelFormatTypes: [FourCharCode] = [] + + var connectionWithMediaTypeStub: ((AVMediaType) -> CaptureConnection?)? + + func connection(with mediaType: AVMediaType) -> CaptureConnection? { + return connectionWithMediaTypeStub?(mediaType) + } + + func setSampleBufferDelegate( + _ sampleBufferDelegate: AVCaptureVideoDataOutputSampleBufferDelegate?, + queue sampleBufferCallbackQueue: DispatchQueue? + ) {} +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockDeviceOrientationProvider.swift b/camera_avfoundation/darwin/Tests/Mocks/MockDeviceOrientationProvider.swift new file mode 100644 index 0000000..034eafc --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockDeviceOrientationProvider.swift @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import UIKit + +@testable import camera_avfoundation + +final class MockDeviceOrientationProvider: NSObject, DeviceOrientationProvider { + var orientationStub: (() -> UIDeviceOrientation)? + + var orientation: UIDeviceOrientation { + return orientationStub?() ?? .unknown + } +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockFLTCameraPermissionManager.swift b/camera_avfoundation/darwin/Tests/Mocks/MockFLTCameraPermissionManager.swift new file mode 100644 index 0000000..73f991f --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockFLTCameraPermissionManager.swift @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Flutter + +@testable import camera_avfoundation + +final class MockCameraPermissionManager: CameraPermissionManager { + var requestCameraPermissionStub: ((@escaping CameraPermissionRequestCompletionHandler) -> Void)? + var requestAudioPermissionStub: ((@escaping CameraPermissionRequestCompletionHandler) -> Void)? + + init() { + super.init(permissionService: DefaultPermissionService()) + } + + override func requestCameraPermission(completionHandler: @escaping (PigeonError?) -> Void) { + if let stub = requestCameraPermissionStub { + stub(completionHandler) + } else { + super.requestCameraPermission(completionHandler: completionHandler) + } + } + + override func requestAudioPermission(completionHandler: @escaping (PigeonError?) -> Void) { + if let stub = requestAudioPermissionStub { + stub(completionHandler) + } else { + super.requestAudioPermission(completionHandler: completionHandler) + } + } +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockFlutterBinaryMessenger.swift b/camera_avfoundation/darwin/Tests/Mocks/MockFlutterBinaryMessenger.swift new file mode 100644 index 0000000..7aad980 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockFlutterBinaryMessenger.swift @@ -0,0 +1,26 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Flutter + +/// Mocked implementation of `FlutterBinaryMessenger` protocol that exists to allow constructing +/// a `CameraPlugin` instance for testing. It contains an empty implementation for all protocol +/// methods. +final class MockFlutterBinaryMessenger: NSObject, FlutterBinaryMessenger { + func send(onChannel channel: String, message: Data?) {} + + func send( + onChannel channel: + String, + message: + Data?, binaryReply callback: FlutterBinaryReply? = nil + ) {} + + func setMessageHandlerOnChannel( + _ channel: String, + binaryMessageHandler handler: FlutterBinaryMessageHandler? = nil + ) -> FlutterBinaryMessengerConnection { 0 } + + func cleanUpConnection(_ connection: FlutterBinaryMessengerConnection) {} +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockFlutterTextureRegistry.swift b/camera_avfoundation/darwin/Tests/Mocks/MockFlutterTextureRegistry.swift new file mode 100644 index 0000000..a798e55 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockFlutterTextureRegistry.swift @@ -0,0 +1,16 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Flutter + +/// Mocked implementation of `FlutterTextureRegistry` protocol that exists to allow constructing +/// a `CameraPlugin` instance for testing. It contains an empty implementation for all protocol +/// methods. +final class MockFlutterTextureRegistry: NSObject, FlutterTextureRegistry { + func register(_ texture: FlutterTexture) -> Int64 { 0 } + + func textureFrameAvailable(_ textureId: Int64) {} + + func unregisterTexture(_ textureId: Int64) {} +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockFrameRateRange.swift b/camera_avfoundation/darwin/Tests/Mocks/MockFrameRateRange.swift new file mode 100644 index 0000000..d4bfbda --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockFrameRateRange.swift @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Foundation + +@testable import camera_avfoundation + +/// A mock implementation of `FrameRateRange` that allows mocking the class properties. +final class MockFrameRateRange: NSObject, FrameRateRange { + var minFrameRate: Float64 + var maxFrameRate: Float64 + + /// Initializes a `MockFrameRateRange` with the given frame rate range. + init(minFrameRate: Float64, maxFrameRate: Float64) { + self.minFrameRate = minFrameRate + self.maxFrameRate = maxFrameRate + } +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockGlobalEventApi.swift b/camera_avfoundation/darwin/Tests/Mocks/MockGlobalEventApi.swift new file mode 100644 index 0000000..1a95e42 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockGlobalEventApi.swift @@ -0,0 +1,26 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Flutter + +@testable import camera_avfoundation + +/// A mock implementation of `CameraGlobalEventApi` that captures received +/// `deviceOrientationChanged` events and exposes whether they were received to the testing code. +final class MockGlobalEventApi: CameraGlobalEventApiProtocol { + /// Whether the `deviceOrientationChanged` callback was called. + var deviceOrientationChangedCalled = false + + /// The last orientation received by the `deviceOrientationChanged` callback. + var lastOrientation = PlatformDeviceOrientation.portraitUp + + func deviceOrientationChanged( + orientation orientationArg: PlatformDeviceOrientation, + completion: @escaping (Result) -> Void + ) { + deviceOrientationChangedCalled = true + lastOrientation = orientationArg + completion(.success(())) + } +} diff --git a/camera_avfoundation/darwin/Tests/Mocks/MockWritableData.swift b/camera_avfoundation/darwin/Tests/Mocks/MockWritableData.swift new file mode 100644 index 0000000..8666c4d --- /dev/null +++ b/camera_avfoundation/darwin/Tests/Mocks/MockWritableData.swift @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Foundation + +@testable import camera_avfoundation + +/// A mock implementation of `WritableData` that allows injecting a custom implementation +/// for writing to a file. +final class MockWritableData: WritableData { + var writeToFileStub: ((String, Data.WritingOptions) throws -> Void)? + + func writeToPath(_ path: String, options: Data.WritingOptions) throws { + if let stub = self.writeToFileStub { + try stub(path, options) + } + } +} diff --git a/camera_avfoundation/darwin/Tests/PhotoCaptureTests.swift b/camera_avfoundation/darwin/Tests/PhotoCaptureTests.swift new file mode 100644 index 0000000..1113fb4 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/PhotoCaptureTests.swift @@ -0,0 +1,248 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import XCTest + +@testable import camera_avfoundation + +/// Includes test cases related to photo capture operations for Camera class. +final class PhotoCaptureTests: XCTestCase { + private func createCam(with captureSessionQueue: DispatchQueue) -> DefaultCamera { + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.captureSessionQueue = captureSessionQueue + return CameraTestUtils.createTestCamera(configuration) + } + + func testCaptureToFile_mustReportErrorToResultIfSavePhotoDelegateCompletionsWithError() { + let errorExpectation = expectation( + description: "Must send error to result if save photo delegate completes with error.") + let captureSessionQueue = DispatchQueue(label: "capture_session_queue") + captureSessionQueue.setSpecific( + key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue) + let cam = createCam(with: captureSessionQueue) + let error = NSError(domain: "test", code: 0, userInfo: nil) + + let mockOutput = MockCapturePhotoOutput() + mockOutput.capturePhotoWithSettingsStub = { settings, photoDelegate in + let delegate = + cam.inProgressSavePhotoDelegates[settings.uniqueID] + // Completion runs on IO queue. + let ioQueue = DispatchQueue(label: "io_queue") + ioQueue.async { + delegate?.completionHandler(nil, error) + } + } + cam.capturePhotoOutput = mockOutput + + // `Camera.captureToFile` runs on capture session queue. + captureSessionQueue.async { + cam.captureToFile { result in + switch result { + case .success(_): + XCTFail("Expected failure") + case .failure(_): + break + } + errorExpectation.fulfill() + } + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWithPath() { + let pathExpectation = expectation( + description: "Must send file path to result if save photo delegate completes with file path.") + let captureSessionQueue = DispatchQueue(label: "capture_session_queue") + captureSessionQueue.setSpecific( + key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue) + let cam = createCam(with: captureSessionQueue) + let filePath = "test" + + let mockOutput = MockCapturePhotoOutput() + mockOutput.capturePhotoWithSettingsStub = { settings, photoDelegate in + let delegate = + cam.inProgressSavePhotoDelegates[settings.uniqueID] + // Completion runs on IO queue. + let ioQueue = DispatchQueue(label: "io_queue") + ioQueue.async { + delegate?.completionHandler(filePath, nil) + } + } + cam.capturePhotoOutput = mockOutput + + // `Camera.captureToFile` runs on capture session queue. + captureSessionQueue.async { + cam.captureToFile { result in + switch result { + case .success(let result): + XCTAssertEqual(result, filePath) + case .failure(_): + XCTFail("Unexpected failure") + } + pathExpectation.fulfill() + } + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testCaptureToFile_mustReportFileExtensionWithHeifWhenHEVCIsAvailableAndFileFormatIsHEIF() { + let expectation = self.expectation( + description: "Test must set extension to heif if availablePhotoCodecTypes contains HEVC.") + + let captureSessionQueue = DispatchQueue(label: "capture_session_queue") + captureSessionQueue.setSpecific( + key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue) + let cam = createCam(with: captureSessionQueue) + cam.setImageFileFormat(PlatformImageFileFormat.heif) + + let mockOutput = MockCapturePhotoOutput() + mockOutput.availablePhotoCodecTypes = [AVVideoCodecType.hevc] + mockOutput.capturePhotoWithSettingsStub = { settings, photoDelegate in + let delegate = + cam.inProgressSavePhotoDelegates[settings.uniqueID] + // Completion runs on IO queue. + let ioQueue = DispatchQueue(label: "io_queue") + ioQueue.async { + delegate?.completionHandler(delegate?.filePath, nil) + } + } + cam.capturePhotoOutput = mockOutput + + // `Camera.captureToFile` runs on capture session queue. + captureSessionQueue.async { + cam.captureToFile { result in + if let filePath = self.assertSuccess(result) { + XCTAssertEqual((filePath as NSString).pathExtension, "heif") + } + expectation.fulfill() + } + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testCaptureToFile_mustReportFileExtensionWithJpgWhenHEVCNotAvailableAndFileFormatIsHEIF() { + let expectation = self.expectation( + description: + "Test must set extension to jpg if availablePhotoCodecTypes does not contain HEVC.") + + let captureSessionQueue = DispatchQueue(label: "capture_session_queue") + captureSessionQueue.setSpecific( + key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue) + let cam = createCam(with: captureSessionQueue) + cam.setImageFileFormat(PlatformImageFileFormat.heif) + + let mockOutput = MockCapturePhotoOutput() + mockOutput.capturePhotoWithSettingsStub = { settings, photoDelegate in + let delegate = + cam.inProgressSavePhotoDelegates[settings.uniqueID] + // Completion runs on IO queue. + let ioQueue = DispatchQueue(label: "io_queue") + ioQueue.async { + delegate?.completionHandler(delegate?.filePath, nil) + } + } + cam.capturePhotoOutput = mockOutput + + // `Camera.captureToFile` runs on capture session queue. + captureSessionQueue.async { + cam.captureToFile { result in + if let filePath = self.assertSuccess(result) { + XCTAssertEqual((filePath as NSString).pathExtension, "jpg") + } + expectation.fulfill() + } + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testCaptureToFile_handlesTorchMode() { + let pathExpectation = expectation( + description: "Must send file path to result if save photo delegate completes with file path.") + let setTorchExpectation = expectation( + description: "Should set torch mode to AVCaptureTorchModeOn.") + + let captureDeviceMock = MockCaptureDevice() + captureDeviceMock.hasTorch = true + captureDeviceMock.isTorchAvailable = true + captureDeviceMock.getTorchModeStub = { .auto } + captureDeviceMock.setTorchModeStub = { mode in + if mode == .on { + setTorchExpectation.fulfill() + } + } + + let captureSessionQueue = DispatchQueue(label: "capture_session_queue") + captureSessionQueue.setSpecific( + key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue) + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.captureSessionQueue = captureSessionQueue + configuration.videoCaptureDeviceFactory = { _ in captureDeviceMock } + let cam = CameraTestUtils.createTestCamera(configuration) + + let filePath = "test" + let mockOutput = MockCapturePhotoOutput() + mockOutput.capturePhotoWithSettingsStub = { settings, photoDelegate in + let delegate = + cam.inProgressSavePhotoDelegates[settings.uniqueID] + // Completion runs on IO queue. + let ioQueue = DispatchQueue(label: "io_queue") + ioQueue.async { + delegate?.completionHandler(filePath, nil) + } + } + cam.capturePhotoOutput = mockOutput + + // `Camera.captureToFile` runs on capture session queue. + captureSessionQueue.async { + cam.setFlashMode(.torch) { _ in } + cam.captureToFile { result in + if let result = self.assertSuccess(result) { + XCTAssertEqual(result, filePath) + } + pathExpectation.fulfill() + } + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testCaptureToFile_mustSavePhotoToCameraPicturesDirectory() { + let expectedPath = "/tmp/camera/pictures/" + let expectation = self.expectation( + description: "Photo must be saved to \(expectedPath) directory.") + + let captureSessionQueue = DispatchQueue(label: "capture_session_queue") + captureSessionQueue.setSpecific( + key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue) + let cam = createCam(with: captureSessionQueue) + + let mockOutput = MockCapturePhotoOutput() + mockOutput.capturePhotoWithSettingsStub = { settings, photoDelegate in + let delegate = cam.inProgressSavePhotoDelegates[settings.uniqueID] + let ioQueue = DispatchQueue(label: "io_queue") + ioQueue.async { + delegate?.completionHandler(delegate?.filePath, nil) + } + } + cam.capturePhotoOutput = mockOutput + + captureSessionQueue.async { + cam.captureToFile { result in + if let filePath = self.assertSuccess(result) { + XCTAssertTrue( + filePath.contains(expectedPath) + ) + } + expectation.fulfill() + } + } + + waitForExpectations(timeout: 30, handler: nil) + } +} diff --git a/camera_avfoundation/darwin/Tests/QueueUtilsTests.swift b/camera_avfoundation/darwin/Tests/QueueUtilsTests.swift new file mode 100644 index 0000000..29a7cc5 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/QueueUtilsTests.swift @@ -0,0 +1,35 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import XCTest + +@testable import camera_avfoundation + +final class QueueUtilsTests: XCTestCase { + func testShouldStayOnMainQueueIfCalledFromMainQueue() { + let expectation = expectation(description: "Block must be run on the main queue") + + ensureToRunOnMainQueue { + if Thread.isMainThread { + expectation.fulfill() + } + } + + waitForExpectations(timeout: 30) + } + + func testShouldDispatchToMainQueueIfCalledFromBackgroundQueue() { + let expectation = expectation(description: "Block must be run on the main queue") + + DispatchQueue.global(qos: .default).async { + ensureToRunOnMainQueue { + if Thread.isMainThread { + expectation.fulfill() + } + } + } + + waitForExpectations(timeout: 30) + } +} diff --git a/camera_avfoundation/darwin/Tests/SampleBufferTests.swift b/camera_avfoundation/darwin/Tests/SampleBufferTests.swift new file mode 100644 index 0000000..98759f1 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/SampleBufferTests.swift @@ -0,0 +1,542 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import XCTest + +@testable import camera_avfoundation + +private class FakeMediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper { + let inputMock: MockAssetWriterInput + + init(inputMock: MockAssetWriterInput) { + self.inputMock = inputMock + } + + override func lockDevice(_ captureDevice: CaptureDevice) throws { + // No-op. + } + + override func unlockDevice(_ captureDevice: CaptureDevice) { + // No-op. + } + + override func beginConfiguration(for videoCaptureSession: CaptureSession) { + // No-op. + } + + override func commitConfiguration(for videoCaptureSession: CaptureSession) { + // No-op. + } + + override func setMinFrameDuration(_ duration: CMTime, on captureDevice: CaptureDevice) { + // No-op. + } + + override func setMaxFrameDuration(_ duration: CMTime, on captureDevice: CaptureDevice) { + // No-op. + } + + override func assetWriterAudioInput(withOutputSettings outputSettings: [String: Any]?) + -> AssetWriterInput + { + return inputMock + } + + override func assetWriterVideoInput(withOutputSettings outputSettings: [String: Any]?) + -> AssetWriterInput + { + return inputMock + } + + override func addInput(_ writerInput: AssetWriterInput, to writer: AssetWriter) { + // No-op. + } + + override func recommendedVideoSettingsForAssetWriter( + withFileType fileType: AVFileType, for output: CaptureVideoDataOutput + ) -> [String: Any]? { + return [:] + } +} + +/// Includes test cases related to sample buffer handling for Camera class. +final class CameraSampleBufferTests: XCTestCase { + private func createCamera() -> ( + DefaultCamera, + MockAssetWriter, + MockAssetWriterInputPixelBufferAdaptor, + MockAssetWriterInput + ) { + let assetWriter = MockAssetWriter() + let adaptor = MockAssetWriterInputPixelBufferAdaptor() + let input = MockAssetWriterInput() + + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.mediaSettings = PlatformMediaSettings( + resolutionPreset: .medium, + framesPerSecond: nil, + videoBitrate: nil, + audioBitrate: nil, + enableAudio: true) + configuration.mediaSettingsWrapper = FakeMediaSettingsAVWrapper(inputMock: input) + + configuration.assetWriterFactory = { url, fileType in + return assetWriter + } + configuration.inputPixelBufferAdaptorFactory = { input, settings in + return adaptor + } + + return ( + CameraTestUtils.createTestCamera(configuration), + assetWriter, + adaptor, + input + ) + } + + func testSampleBufferCallbackQueueMustBeCaptureSessionQueue() { + let captureSessionQueue = DispatchQueue(label: "testing") + let camera = CameraTestUtils.createCameraWithCaptureSessionQueue(captureSessionQueue) + XCTAssertEqual( + captureSessionQueue, camera.captureVideoOutput.avOutput.sampleBufferCallbackQueue, + "Sample buffer callback queue must be the capture session queue.") + } + + func testCopyPixelBuffer() { + let (camera, _, _, _) = createCamera() + let capturedSampleBuffer = CameraTestUtils.createTestSampleBuffer() + let capturedPixelBuffer = CMSampleBufferGetImageBuffer(capturedSampleBuffer)! + let testConnection = CameraTestUtils.createTestConnection(camera.captureVideoOutput.avOutput) + + // Mimic sample buffer callback when captured a new video sample. + camera.captureOutput( + camera.captureVideoOutput.avOutput, + didOutput: capturedSampleBuffer, + from: testConnection) + let deliveredPixelBuffer = camera.copyPixelBuffer()?.takeRetainedValue() + XCTAssertEqual( + deliveredPixelBuffer, capturedPixelBuffer, + "Camera must deliver the latest captured pixel buffer to copyPixelBuffer API.") + } + + func testDidOutputSampleBuffer_mustNotChangeSampleBufferRetainCountAfterPauseResumeRecording() { + let (camera, _, _, _) = createCamera() + let sampleBuffer = CameraTestUtils.createTestSampleBuffer() + let testConnection = CameraTestUtils.createTestConnection(camera.captureVideoOutput.avOutput) + + let initialRetainCount = CFGetRetainCount(sampleBuffer) + + // Pause then resume the recording. + camera.startVideoRecording(completion: { error in }, messengerForStreaming: nil) + camera.pauseVideoRecording() + camera.resumeVideoRecording() + + camera.captureOutput( + camera.captureVideoOutput.avOutput, + didOutput: sampleBuffer, + from: testConnection) + + let finalRetainCount = CFGetRetainCount(sampleBuffer) + XCTAssertEqual( + finalRetainCount, initialRetainCount, + "didOutputSampleBuffer must not change the sample buffer retain count after pause resume recording." + ) + } + + func testDidOutputSampleBufferIgnoreAudioSamplesBeforeVideoSamples() { + let (camera, writerMock, adaptorMock, inputMock) = createCamera() + var status = AVAssetWriter.Status.unknown + writerMock.startWritingStub = { + status = .writing + return true + } + writerMock.statusStub = { + return status + } + + let videoSample = CameraTestUtils.createTestSampleBuffer() + let testVideoConnection = CameraTestUtils.createTestConnection( + camera.captureVideoOutput.avOutput) + + let audioSample = CameraTestUtils.createTestAudioSampleBuffer() + let testAudioOutput = CameraTestUtils.createTestAudioOutput() + let testAudioConnection = CameraTestUtils.createTestConnection(testAudioOutput) + + var writtenSamples: [String] = [] + adaptorMock.appendStub = { buffer, time in + writtenSamples.append("video") + return true + } + inputMock.isReadyForMoreMediaData = true + inputMock.appendStub = { buffer in + writtenSamples.append("audio") + return true + } + + camera.startVideoRecording(completion: { error in }, messengerForStreaming: nil) + camera.captureOutput(testAudioOutput, didOutput: audioSample, from: testAudioConnection) + camera.captureOutput(testAudioOutput, didOutput: audioSample, from: testAudioConnection) + camera.captureOutput( + camera.captureVideoOutput.avOutput, + didOutput: videoSample, + from: testVideoConnection) + camera.captureOutput(testAudioOutput, didOutput: audioSample, from: testAudioConnection) + + let expectedSamples = ["video", "audio"] + XCTAssertEqual(writtenSamples, expectedSamples, "First appended sample must be video.") + } + + func testDidOutputSampleBufferSampleTimesMustBeNumericAfterPauseResume() { + let (camera, writerMock, adaptorMock, inputMock) = createCamera() + + let videoSample = CameraTestUtils.createTestSampleBuffer() + let testVideoConnection = CameraTestUtils.createTestConnection( + camera.captureVideoOutput.avOutput) + + let audioSample = CameraTestUtils.createTestAudioSampleBuffer() + let testAudioOutput = CameraTestUtils.createTestAudioOutput() + let testAudioConnection = CameraTestUtils.createTestConnection(testAudioOutput) + + var status = AVAssetWriter.Status.unknown + writerMock.startWritingStub = { + status = .writing + return true + } + writerMock.statusStub = { + return status + } + + var videoAppended = false + adaptorMock.appendStub = { buffer, time in + XCTAssert(CMTIME_IS_NUMERIC(time)) + videoAppended = true + return true + } + + var audioAppended = false + inputMock.isReadyForMoreMediaData = true + inputMock.appendStub = { buffer in + let sampleTime = CMSampleBufferGetPresentationTimeStamp(buffer) + XCTAssert(CMTIME_IS_NUMERIC(sampleTime)) + audioAppended = true + return true + } + + camera.startVideoRecording(completion: { error in }, messengerForStreaming: nil) + camera.pauseVideoRecording() + camera.resumeVideoRecording() + camera.captureOutput( + camera.captureVideoOutput.avOutput, + didOutput: videoSample, + from: testVideoConnection) + camera.captureOutput(testAudioOutput, didOutput: audioSample, from: testAudioConnection) + camera.captureOutput( + camera.captureVideoOutput.avOutput, + didOutput: videoSample, + from: testVideoConnection) + camera.captureOutput(testAudioOutput, didOutput: audioSample, from: testAudioConnection) + + XCTAssert(videoAppended && audioAppended, "Video or audio was not appended.") + } + + func testDidOutputSampleBufferMustNotAppendSampleWhenReadyForMoreMediaDataIsFalse() { + let (camera, _, adaptorMock, inputMock) = createCamera() + + let videoSample = CameraTestUtils.createTestSampleBuffer() + let testVideoConnection = CameraTestUtils.createTestConnection( + camera.captureVideoOutput.avOutput) + + var sampleAppended = false + adaptorMock.appendStub = { buffer, time in + sampleAppended = true + return true + } + + camera.startVideoRecording(completion: { error in }, messengerForStreaming: nil) + + inputMock.isReadyForMoreMediaData = true + sampleAppended = false + camera.captureOutput( + camera.captureVideoOutput.avOutput, + didOutput: videoSample, + from: testVideoConnection) + XCTAssertTrue(sampleAppended, "Sample was not appended.") + + inputMock.isReadyForMoreMediaData = false + sampleAppended = false + camera.captureOutput( + camera.captureVideoOutput.avOutput, + didOutput: videoSample, + from: testVideoConnection) + XCTAssertFalse(sampleAppended, "Sample cannot be appended when readyForMoreMediaData is NO.") + } + + func testStopVideoRecordingWithCompletionMustCallCompletion() { + let (camera, writerMock, _, _) = createCamera() + + var status = AVAssetWriter.Status.unknown + writerMock.startWritingStub = { + status = .writing + return true + } + writerMock.statusStub = { + return status + } + writerMock.finishWritingStub = { handler in + XCTAssert( + writerMock.status == .writing, + "Cannot call finishWritingWithCompletionHandler when status is not AVAssetWriter.Status.writing." + ) + handler() + } + + camera.startVideoRecording(completion: { error in }, messengerForStreaming: nil) + var completionCalled = false + camera.stopVideoRecording(completion: { result in + completionCalled = true + }) + + XCTAssert(completionCalled, "Completion was not called.") + } + + func testStartWritingShouldNotBeCalledBetweenSampleCreationAndAppending() { + let (camera, writerMock, adaptorMock, inputMock) = createCamera() + + let videoSample = CameraTestUtils.createTestSampleBuffer() + let testVideoConnection = CameraTestUtils.createTestConnection( + camera.captureVideoOutput.avOutput) + + var startWritingCalled = false + writerMock.startWritingStub = { + startWritingCalled = true + return true + + } + + var videoAppended = false + adaptorMock.appendStub = { buffer, time in + videoAppended = true + return true + } + + inputMock.isReadyForMoreMediaData = true + + camera.startVideoRecording(completion: { error in }, messengerForStreaming: nil) + + let startWritingCalledBefore = startWritingCalled + camera.captureOutput( + camera.captureVideoOutput.avOutput, + didOutput: videoSample, + from: testVideoConnection) + XCTAssert( + (startWritingCalledBefore && videoAppended) || (startWritingCalled && !videoAppended), + "The startWriting was called between sample creation and appending.") + + camera.captureOutput( + camera.captureVideoOutput.avOutput, + didOutput: videoSample, + from: testVideoConnection) + XCTAssert(videoAppended, "Video was not appended.") + } + + func testStartVideoRecordingWithCompletionShouldNotDisableMixWithOthers() { + let cam = CameraTestUtils.createCameraWithCaptureSessionQueue(DispatchQueue(label: "testing")) + + try? AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers) + cam.startVideoRecording(completion: { error in }, messengerForStreaming: nil) + XCTAssert( + AVAudioSession.sharedInstance().categoryOptions.contains(.mixWithOthers), + "Flag MixWithOthers was removed.") + XCTAssert( + AVAudioSession.sharedInstance().category == .playAndRecord, + "Category should be PlayAndRecord.") + } + + func testDidOutputSampleBufferMustUseSingleOffsetForVideoAndAudio() { + let (camera, writerMock, adaptorMock, inputMock) = createCamera() + + let testVideoConnection = CameraTestUtils.createTestConnection( + camera.captureVideoOutput.avOutput) + let testAudioOutput = CameraTestUtils.createTestAudioOutput() + let testAudioConnection = CameraTestUtils.createTestConnection(testAudioOutput) + + var status = AVAssetWriter.Status.unknown + writerMock.startWritingStub = { + status = .writing + return true + } + writerMock.statusStub = { + return status + } + + var appendedTime = CMTime.invalid + + adaptorMock.appendStub = { buffer, time in + appendedTime = time + return true + } + + inputMock.isReadyForMoreMediaData = true + inputMock.appendStub = { buffer in + appendedTime = CMSampleBufferGetPresentationTimeStamp(buffer) + return true + } + + camera.startVideoRecording(completion: { error in }, messengerForStreaming: nil) + + let appendVideoSample = { (time: Int64) in + camera.captureOutput( + camera.captureVideoOutput.avOutput, + didOutput: CameraTestUtils.createTestSampleBuffer( + timestamp: CMTimeMake(value: time, timescale: 1), + duration: .invalid), + from: testVideoConnection) + } + + let appendAudioSample = { (time: Int64, duration: Int64) in + camera.captureOutput( + testAudioOutput, + didOutput: CameraTestUtils.createTestAudioSampleBuffer( + timestamp: CMTimeMake(value: time, timescale: 1), + duration: CMTimeMake(value: duration, timescale: 1)), + from: testAudioConnection) + } + + appendedTime = .invalid + camera.pauseVideoRecording() + camera.resumeVideoRecording() + appendVideoSample(1) + XCTAssertEqual(appendedTime, CMTimeMake(value: 1, timescale: 1)) + + appendedTime = .invalid + camera.pauseVideoRecording() + camera.resumeVideoRecording() + appendVideoSample(11) + XCTAssertEqual(appendedTime, .invalid) + appendVideoSample(12) + XCTAssertEqual(appendedTime, CMTimeMake(value: 2, timescale: 1)) + + appendedTime = .invalid + camera.pauseVideoRecording() + camera.resumeVideoRecording() + appendAudioSample(20, 2) + XCTAssertEqual(appendedTime, .invalid) + appendVideoSample(23) + XCTAssertEqual(appendedTime, CMTimeMake(value: 3, timescale: 1)) + + appendedTime = .invalid + camera.pauseVideoRecording() + camera.resumeVideoRecording() + appendVideoSample(28) + XCTAssertEqual(appendedTime, .invalid) + appendAudioSample(30, 2) + XCTAssertEqual(appendedTime, .invalid) + appendVideoSample(33) + XCTAssertEqual(appendedTime, .invalid) + appendAudioSample(32, 2) + XCTAssertEqual(appendedTime, CMTimeMake(value: 2, timescale: 1)) + } + + func testDidOutputSampleBufferMustConnectVideoAfterSessionInterruption() { + let (camera, writerMock, adaptorMock, inputMock) = createCamera() + + let testVideoConnection = CameraTestUtils.createTestConnection( + camera.captureVideoOutput.avOutput) + let testAudioOutput = CameraTestUtils.createTestAudioOutput() + let testAudioConnection = CameraTestUtils.createTestConnection(testAudioOutput) + + var status = AVAssetWriter.Status.unknown + writerMock.startWritingStub = { + status = .writing + return true + } + writerMock.statusStub = { + return status + } + + var appendedTime = CMTime.invalid + + adaptorMock.appendStub = { buffer, time in + appendedTime = time + return true + } + + inputMock.isReadyForMoreMediaData = true + inputMock.appendStub = { buffer in + appendedTime = CMSampleBufferGetPresentationTimeStamp(buffer) + return true + } + + camera.startVideoRecording(completion: { error in }, messengerForStreaming: nil) + + let appendVideoSample = { (time: Int64) in + camera.captureOutput( + camera.captureVideoOutput.avOutput, + didOutput: CameraTestUtils.createTestSampleBuffer( + timestamp: CMTimeMake(value: time, timescale: 1), + duration: .invalid), + from: testVideoConnection) + } + + let appendAudioSample = { (time: Int64, duration: Int64) in + camera.captureOutput( + testAudioOutput, + didOutput: CameraTestUtils.createTestAudioSampleBuffer( + timestamp: CMTimeMake(value: time, timescale: 1), + duration: CMTimeMake(value: duration, timescale: 1)), + from: testAudioConnection) + } + + appendVideoSample(1) + appendAudioSample(1, 1) + + NotificationCenter.default.post( + name: AVCaptureSession.wasInterruptedNotification, + object: camera.audioCaptureSession) + + appendedTime = .invalid + appendAudioSample(11, 1) + XCTAssertEqual(appendedTime, .invalid) + appendVideoSample(12) + XCTAssertEqual(appendedTime, CMTimeMake(value: 2, timescale: 1)) + appendedTime = .invalid + appendAudioSample(12, 1) + XCTAssertEqual(appendedTime, CMTimeMake(value: 2, timescale: 1)) + } + + func testStartVideoRecordingReportsErrorWhenStartWritingFails() { + let (camera, writerMock, _, _) = createCamera() + + // Configure the mock to return false for startWriting + writerMock.startWritingStub = { + return false + } + + // Configure a mock error + let mockError = NSError( + domain: "test", code: 123, userInfo: [NSLocalizedDescriptionKey: "Mock write error"]) + writerMock.error = mockError + + let expectation = self.expectation(description: "Completion handler called with error") + + camera.startVideoRecording( + completion: { result in + switch result { + case .failure(let error as PigeonError): + XCTAssertEqual(error.code, "IOError") + XCTAssertEqual(error.message, "AVAssetWriter failed to start writing") + XCTAssertEqual(error.details as? String, "Mock write error") + XCTAssertFalse(camera.isRecording) + expectation.fulfill() + default: + XCTFail("Expected PigeonError") + } + + }, messengerForStreaming: nil) + + waitForExpectations(timeout: 1.0, handler: nil) + } +} diff --git a/camera_avfoundation/darwin/Tests/SavePhotoDelegateTests.swift b/camera_avfoundation/darwin/Tests/SavePhotoDelegateTests.swift new file mode 100644 index 0000000..76b4430 --- /dev/null +++ b/camera_avfoundation/darwin/Tests/SavePhotoDelegateTests.swift @@ -0,0 +1,99 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import XCTest + +@testable import camera_avfoundation + +final class SavePhotoDelegateTests: XCTestCase { + func testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToCapture() { + let completionExpectation = expectation( + description: "Must complete with error if failed to capture photo.") + let captureError = NSError(domain: "test", code: 0, userInfo: nil) + let ioQueue = DispatchQueue(label: "test") + let delegate = SavePhotoDelegate(path: "test", ioQueue: ioQueue) { path, error in + XCTAssertEqual(captureError, error as NSError?) + XCTAssertNil(path) + completionExpectation.fulfill() + } + + delegate.handlePhotoCaptureResult(error: captureError) { nil } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToWrite() { + let completionExpectation = expectation( + description: "Must complete with error if failed to write file.") + let ioQueue = DispatchQueue(label: "test") + let ioError = NSError( + domain: "IOError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Localized IO Error"]) + let delegate = SavePhotoDelegate(path: "test", ioQueue: ioQueue) { path, error in + XCTAssertEqual(ioError, error as NSError?) + XCTAssertNil(path) + completionExpectation.fulfill() + } + + let mockWritableData = MockWritableData() + mockWritableData.writeToFileStub = { path, options in + throw ioError + } + + delegate.handlePhotoCaptureResult(error: nil) { mockWritableData } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testHandlePhotoCaptureResult_mustCompleteWithFilePathIfSuccessToWrite() { + let completionExpectation = expectation( + description: "Must complete with file path if succeeds to write file.") + let ioQueue = DispatchQueue(label: "test") + let filePath = "test" + let delegate = SavePhotoDelegate(path: filePath, ioQueue: ioQueue) { path, error in + XCTAssertNil(error) + XCTAssertEqual(filePath, path) + completionExpectation.fulfill() + } + + let mockWritableData = MockWritableData() + mockWritableData.writeToFileStub = { path, options in } + + delegate.handlePhotoCaptureResult(error: nil) { mockWritableData } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue() { + let dataProviderQueueExpectation = expectation( + description: "Data provider must run on io queue.") + let writeFileQueueExpectation = expectation(description: "File writing must run on io queue.") + let completionExpectation = expectation( + description: "Must complete with file path if success to write file.") + let ioQueue = DispatchQueue(label: "test") + let ioQueueSpecific = DispatchSpecificKey() + ioQueue.setSpecific(key: ioQueueSpecific, value: ()) + + let mockWritableData = MockWritableData() + mockWritableData.writeToFileStub = { path, options in + if DispatchQueue.getSpecific(key: ioQueueSpecific) != nil { + writeFileQueueExpectation.fulfill() + } + } + + let filePath = "test" + let delegate = SavePhotoDelegate(path: filePath, ioQueue: ioQueue) { path, error in + completionExpectation.fulfill() + } + + delegate.handlePhotoCaptureResult(error: nil) { + if DispatchQueue.getSpecific(key: ioQueueSpecific) != nil { + dataProviderQueueExpectation.fulfill() + } + return mockWritableData + } + + waitForExpectations(timeout: 30, handler: nil) + } +} diff --git a/camera_avfoundation/darwin/Tests/StreamingTests.swift b/camera_avfoundation/darwin/Tests/StreamingTests.swift new file mode 100644 index 0000000..b018a7c --- /dev/null +++ b/camera_avfoundation/darwin/Tests/StreamingTests.swift @@ -0,0 +1,217 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import Flutter +import XCTest + +@testable import camera_avfoundation + +private class MockEventSink: PigeonEventSink { + var eventSinkSuccessStub: ((PlatformCameraImageData?) -> Void)? + + init() { + super.init({ event in }) + } + + override func success(_ event: PlatformCameraImageData?) { + eventSinkSuccessStub?(event) + } + + override func error(code: String, message: String?, details: Any?) {} + + override func endOfStream() {} +} + +private class MockImageStreamHandler: ImageDataStreamStreamHandler, ImageStreamHandler { + var mockEventSink = MockEventSink() + var captureSessionQueue: DispatchQueue { + preconditionFailure("Attempted to access unimplemented property: captureSessionQueue") + } + var eventSink: PigeonEventSink? { + set { + } + get { + return mockEventSink + } + } + + var eventSinkSuccessStub: ((PlatformCameraImageData?) -> Void)? { + set { + mockEventSink.eventSinkSuccessStub = newValue + } + get { + return mockEventSink.eventSinkSuccessStub + } + } + + override func onListen( + withArguments arguments: Any?, sink: PigeonEventSink + ) { + } + + override func onCancel(withArguments arguments: Any?) { + } +} + +final class StreamingTests: XCTestCase { + private func createCamera() -> ( + DefaultCamera, + AVCaptureOutput, + CMSampleBuffer, + CMSampleBuffer, + AVCaptureConnection + ) { + let captureSessionQueue = DispatchQueue(label: "testing") + let configuration = CameraTestUtils.createTestCameraConfiguration() + configuration.captureSessionQueue = captureSessionQueue + + let camera = CameraTestUtils.createTestCamera(configuration) + let testAudioOutput = CameraTestUtils.createTestAudioOutput() + let sampleBuffer = CameraTestUtils.createTestSampleBuffer() + let audioSampleBuffer = CameraTestUtils.createTestAudioSampleBuffer() + let testAudioConnection = CameraTestUtils.createTestConnection(testAudioOutput) + + return (camera, testAudioOutput, sampleBuffer, audioSampleBuffer, testAudioConnection) + } + + func testExceedMaxStreamingPendingFramesCount() { + let (camera, testAudioOutput, sampleBuffer, _, testAudioConnection) = createCamera() + let handlerMock = MockImageStreamHandler() + + let finishStartStreamExpectation = expectation( + description: "Finish startStream") + + let messenger = MockFlutterBinaryMessenger() + camera.startImageStream( + with: messenger, imageStreamHandler: handlerMock, + completion: { + _ in + finishStartStreamExpectation.fulfill() + }) + + waitForExpectations(timeout: 30, handler: nil) + + // Setup mocked event sink after the stream starts + let streamingExpectation = expectation( + description: "Must not call handler over maxStreamingPendingFramesCount") + + handlerMock.eventSinkSuccessStub = { event in + streamingExpectation.fulfill() + } + + waitForQueueRoundTrip(with: DispatchQueue.main) + XCTAssertEqual(camera.isStreamingImages, true) + + streamingExpectation.expectedFulfillmentCount = 4 + for _ in 0..<10 { + camera.captureOutput(testAudioOutput, didOutput: sampleBuffer, from: testAudioConnection) + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testReceivedImageStreamData() { + let (camera, testAudioOutput, sampleBuffer, _, testAudioConnection) = createCamera() + let handlerMock = MockImageStreamHandler() + + let finishStartStreamExpectation = expectation( + description: "Finish startStream") + + let messenger = MockFlutterBinaryMessenger() + camera.startImageStream( + with: messenger, imageStreamHandler: handlerMock, + completion: { + _ in + finishStartStreamExpectation.fulfill() + }) + + waitForExpectations(timeout: 30, handler: nil) + + // Setup mocked event sink after the stream starts + let streamingExpectation = expectation( + description: "Must be able to call the handler again when receivedImageStreamData is called") + handlerMock.eventSinkSuccessStub = { event in + streamingExpectation.fulfill() + } + + waitForQueueRoundTrip(with: DispatchQueue.main) + XCTAssertEqual(camera.isStreamingImages, true) + + streamingExpectation.expectedFulfillmentCount = 5 + for _ in 0..<10 { + camera.captureOutput(testAudioOutput, didOutput: sampleBuffer, from: testAudioConnection) + } + + camera.receivedImageStreamData() + camera.captureOutput(testAudioOutput, didOutput: sampleBuffer, from: testAudioConnection) + + waitForExpectations(timeout: 30, handler: nil) + } + + func testIgnoresNonImageBuffers() { + let (camera, testAudioOutput, _, audioSampleBuffer, testAudioConnection) = createCamera() + let handlerMock = MockImageStreamHandler() + handlerMock.eventSinkSuccessStub = { event in + XCTFail() + } + + let finishStartStreamExpectation = expectation( + description: "Finish startStream") + + let messenger = MockFlutterBinaryMessenger() + camera.startImageStream( + with: messenger, imageStreamHandler: handlerMock, + completion: { + _ in + finishStartStreamExpectation.fulfill() + }) + + waitForExpectations(timeout: 30, handler: nil) + XCTAssertEqual(camera.isStreamingImages, true) + + camera.captureOutput(testAudioOutput, didOutput: audioSampleBuffer, from: testAudioConnection) + + waitForQueueRoundTrip(with: DispatchQueue.main) + } + + func testImageStreamEventFormat() throws { + let (camera, testAudioOutput, sampleBuffer, _, testAudioConnection) = createCamera() + + let expectation = expectation(description: "Received a valid event") + + let handlerMock = MockImageStreamHandler() + handlerMock.eventSinkSuccessStub = { event in + guard let imageBuffer = event else { + XCTFail() + return + } + XCTAssertGreaterThan(imageBuffer.width, 0) + XCTAssertGreaterThan(imageBuffer.height, 0) + XCTAssertGreaterThan(imageBuffer.formatCode, 0) + XCTAssertGreaterThan(imageBuffer.lensAperture, 0) + XCTAssertGreaterThan(imageBuffer.sensorExposureTimeNanoseconds, 0) + XCTAssertGreaterThan(imageBuffer.sensorSensitivity, 0) + + let planes = imageBuffer.planes + let planeBuffer = planes[0] + + XCTAssertGreaterThan(planeBuffer.bytesPerRow, 0) + XCTAssertGreaterThan(planeBuffer.width, 0) + XCTAssertGreaterThan(planeBuffer.height, 0) + XCTAssertGreaterThan(planeBuffer.bytes.data.count, 0) + + expectation.fulfill() + } + let messenger = MockFlutterBinaryMessenger() + camera.startImageStream(with: messenger, imageStreamHandler: handlerMock) { _ in } + + waitForQueueRoundTrip(with: DispatchQueue.main) + XCTAssertEqual(camera.isStreamingImages, true) + + camera.captureOutput(testAudioOutput, didOutput: sampleBuffer, from: testAudioConnection) + + waitForExpectations(timeout: 30, handler: nil) + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation.podspec b/camera_avfoundation/darwin/camera_avfoundation.podspec new file mode 100644 index 0000000..d7e009b --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation.podspec @@ -0,0 +1,27 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'camera_avfoundation' + s.version = '0.0.1' + s.summary = 'Flutter Camera' + s.description = <<-DESC +A Flutter plugin to use the camera from your Flutter app. + DESC + s.homepage = 'https://github.com/flutter/packages' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/packages/tree/main/packages/camera_avfoundation' } + s.documentation_url = 'https://pub.dev/packages/camera_avfoundation' + s.source_files = 'camera_avfoundation/Sources/camera_avfoundation/**/*.swift' + s.swift_version = '5.0' + s.xcconfig = { + 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', + 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', + } + s.dependency 'Flutter' + + s.platform = :ios, '13.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.resource_bundles = {'camera_avfoundation_privacy' => ['camera_avfoundation/Sources/camera_avfoundation/Resources/PrivacyInfo.xcprivacy']} +end diff --git a/camera_avfoundation/darwin/camera_avfoundation/Package.swift b/camera_avfoundation/darwin/camera_avfoundation/Package.swift new file mode 100644 index 0000000..302146b --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 5.9 + +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import PackageDescription + +let package = Package( + name: "camera_avfoundation", + platforms: [ + .iOS("13.0") + ], + products: [ + .library( + name: "camera-avfoundation", targets: ["camera_avfoundation"]) + ], + dependencies: [], + targets: [ + .target( + name: "camera_avfoundation", + path: "Sources/camera_avfoundation", + resources: [ + .process("Resources") + ] + ) + ] +) diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/AssetWriter.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/AssetWriter.swift new file mode 100644 index 0000000..d86662a --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/AssetWriter.swift @@ -0,0 +1,45 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import Foundation + +/// A protocol that is a direct passthrough to `AVAssetWriter`. It is used to allow for mocking +/// `AVAssetWriter` in tests. +protocol AssetWriter: NSObjectProtocol { + var status: AVAssetWriter.Status { get } + var error: Error? { get } + + func startWriting() -> Bool + func finishWriting(completionHandler handler: @escaping @Sendable () -> Void) + func startSession(atSourceTime startTime: CMTime) + func add(_ input: AVAssetWriterInput) +} + +/// A protocol that is a direct passthrough to `AVAssetWriterInput`. It is used to allow for mocking +/// `AVAssetWriterInput` in tests. +protocol AssetWriterInput: NSObjectProtocol { + /// The underlying `AVAssetWriterInput` instance. This exists so that the input + /// can be extracted when adding to an AVAssetWriter. + var avInput: AVAssetWriterInput { get } + + var expectsMediaDataInRealTime: Bool { get set } + var isReadyForMoreMediaData: Bool { get } + + func append(_ sampleBuffer: CMSampleBuffer) -> Bool +} + +/// A protocol that is a direct passthrough to `AVAssetWriterInputPixelBufferAdaptor`. It is used to +/// allow for mocking `AVAssetWriterInputPixelBufferAdaptor` in tests. +protocol AssetWriterInputPixelBufferAdaptor: NSObjectProtocol { + func append(_ pixelBuffer: CVPixelBuffer, withPresentationTime presentationTime: CMTime) -> Bool +} + +extension AVAssetWriter: AssetWriter {} + +extension AVAssetWriterInput: AssetWriterInput { + var avInput: AVAssetWriterInput { self } +} + +extension AVAssetWriterInputPixelBufferAdaptor: AssetWriterInputPixelBufferAdaptor {} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/Camera.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/Camera.swift new file mode 100644 index 0000000..02e9503 --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/Camera.swift @@ -0,0 +1,133 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import CoreMotion +import Flutter + +/// A class that manages camera's state and performs camera operations. +protocol Camera: FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate, + AVCaptureAudioDataOutputSampleBufferDelegate +{ + /// The API instance used to communicate with the Dart side of the plugin. + /// Once initially set, this should only ever be accessed on the main thread. + var dartAPI: CameraEventApi? { get set } + + var onFrameAvailable: (() -> Void)? { get set } + + /// Format used for video and image streaming. + var videoFormat: FourCharCode { get set } + + var isPreviewPaused: Bool { get } + var isStreamingImages: Bool { get } + + var deviceOrientation: UIDeviceOrientation { get set } + + var minimumAvailableZoomFactor: CGFloat { get } + var maximumAvailableZoomFactor: CGFloat { get } + var minimumExposureOffset: CGFloat { get } + var maximumExposureOffset: CGFloat { get } + + func setUpCaptureSessionForAudioIfNeeded() + + /// Informs the Dart side of the plugin of the current camera state and capabilities. + func reportInitializationState() + + /// Acknowledges the receipt of one image stream frame. + /// + /// This should be called each time a frame is received. Failing to call it may + /// cause later frames to be dropped instead of streamed. + func receivedImageStreamData() + + func start() + func stop() + + /// Starts recording a video with an optional streaming messenger. + /// If the messenger is non-nil then it will be called for each + /// captured frame, allowing streaming concurrently with recording. + /// + /// @param messenger Nullable messenger for capturing each frame. + func startVideoRecording( + completion: @escaping (Result) -> Void, + messengerForStreaming: FlutterBinaryMessenger? + ) + func pauseVideoRecording() + func resumeVideoRecording() + func stopVideoRecording(completion: @escaping (Result) -> Void) + + func captureToFile(completion: @escaping (Result) -> Void) + + func lockCaptureOrientation(_ orientation: PlatformDeviceOrientation) + func unlockCaptureOrientation() + + func setImageFileFormat(_ fileFormat: PlatformImageFileFormat) + func setJpegImageQuality(_ quality: Int64) + + func setExposureMode(_ mode: PlatformExposureMode) + func setExposureOffset(_ offset: Double) + + /// Sets the exposure point, in a (0,1) coordinate system. + /// + /// If @c point is nil, the exposure point will reset to the center. + func setExposurePoint( + _ point: PlatformPoint?, + withCompletion: @escaping (Result) -> Void + ) + + /// Sets FocusMode on the current AVCaptureDevice. + /// + /// If the @c focusMode is set to FocusModeAuto the AVCaptureDevice is configured to use + /// AVCaptureFocusModeContinuousModeAutoFocus when supported, otherwise it is set to + /// AVCaptureFocusModeAutoFocus. If neither AVCaptureFocusModeContinuousModeAutoFocus nor + /// AVCaptureFocusModeAutoFocus are supported focus mode will not be set. + /// If @c focusMode is set to FocusModeLocked the AVCaptureDevice is configured to use + /// AVCaptureFocusModeAutoFocus. If AVCaptureFocusModeAutoFocus is not supported focus mode will not + /// be set. + /// + /// @param mode The focus mode that should be applied. + func setFocusMode(_ mode: PlatformFocusMode) + + /// Sets the focus point, in a (0,1) coordinate system. + /// + /// If @c point is nil, the focus point will reset to the center. + func setFocusPoint( + _ point: PlatformPoint?, + completion: @escaping (Result) -> Void + ) + + func setZoomLevel(_ zoom: CGFloat, withCompletion: @escaping (Result) -> Void) + + func setVideoStabilizationMode( + _ mode: PlatformVideoStabilizationMode, + withCompletion: @escaping (Result) -> Void) + + func isVideoStabilizationModeSupported(_ mode: PlatformVideoStabilizationMode) -> Bool + + func setFlashMode( + _ mode: PlatformFlashMode, + withCompletion: @escaping (Result) -> Void + ) + + func pausePreview() + func resumePreview() + + func setDescriptionWhileRecording( + _ cameraName: String, + withCompletion: @escaping (Result) -> Void + ) + + func startImageStream( + with: FlutterBinaryMessenger, completion: @escaping (Result) -> Void) + func stopImageStream() + + // Override to make `AVCaptureVideoDataOutputSampleBufferDelegate`/ + // `AVCaptureAudioDataOutputSampleBufferDelegate` method non optional + override func captureOutput( + _ output: AVCaptureOutput, + didOutput sampleBuffer: CMSampleBuffer, + from connection: AVCaptureConnection + ) + + func close() +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraConfiguration.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraConfiguration.swift new file mode 100644 index 0000000..83f8d40 --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraConfiguration.swift @@ -0,0 +1,78 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import CoreMedia +import UIKit + +/// Factory block returning an FLTCaptureDevice. +/// Used in tests to inject a video capture device into DefaultCamera. +typealias VideoCaptureDeviceFactory = (_ cameraName: String) -> CaptureDevice + +typealias AudioCaptureDeviceFactory = () -> CaptureDevice + +typealias CaptureSessionFactory = () -> CaptureSession + +typealias AssetWriterFactory = (_ assetUrl: URL, _ fileType: AVFileType) throws -> AssetWriter + +typealias InputPixelBufferAdaptorFactory = ( + _ input: AssetWriterInput, _ settings: [String: Any]? +) -> + AssetWriterInputPixelBufferAdaptor + +/// A configuration object that centralizes dependencies for `DefaultCamera`. +class CameraConfiguration { + var mediaSettings: PlatformMediaSettings + var mediaSettingsWrapper: FLTCamMediaSettingsAVWrapper + var captureSessionQueue: DispatchQueue + var videoCaptureSession: CaptureSession + var audioCaptureSession: CaptureSession + var videoCaptureDeviceFactory: VideoCaptureDeviceFactory + let audioCaptureDeviceFactory: AudioCaptureDeviceFactory + let captureDeviceInputFactory: CaptureDeviceInputFactory + var assetWriterFactory: AssetWriterFactory + var inputPixelBufferAdaptorFactory: InputPixelBufferAdaptorFactory + var videoDimensionsConverter: VideoDimensionsConverter + var deviceOrientationProvider: DeviceOrientationProvider + let initialCameraName: String + var orientation: UIDeviceOrientation + + init( + mediaSettings: PlatformMediaSettings, + mediaSettingsWrapper: FLTCamMediaSettingsAVWrapper, + captureDeviceFactory: @escaping VideoCaptureDeviceFactory, + audioCaptureDeviceFactory: @escaping AudioCaptureDeviceFactory, + captureSessionFactory: @escaping CaptureSessionFactory, + captureSessionQueue: DispatchQueue, + captureDeviceInputFactory: CaptureDeviceInputFactory, + initialCameraName: String + ) { + self.mediaSettings = mediaSettings + self.mediaSettingsWrapper = mediaSettingsWrapper + self.videoCaptureDeviceFactory = captureDeviceFactory + self.audioCaptureDeviceFactory = audioCaptureDeviceFactory + self.captureSessionQueue = captureSessionQueue + self.videoCaptureSession = captureSessionFactory() + self.audioCaptureSession = captureSessionFactory() + self.captureDeviceInputFactory = captureDeviceInputFactory + self.initialCameraName = initialCameraName + self.orientation = UIDevice.current.orientation + self.deviceOrientationProvider = DefaultDeviceOrientationProvider() + + self.videoDimensionsConverter = { format in + return CMVideoFormatDescriptionGetDimensions(format.formatDescription) + } + + self.assetWriterFactory = { url, fileType in + return try AVAssetWriter(outputURL: url, fileType: fileType) + } + + self.inputPixelBufferAdaptorFactory = { assetWriterInput, sourcePixelBufferAttributes in + return AVAssetWriterInputPixelBufferAdaptor( + assetWriterInput: assetWriterInput.avInput, + sourcePixelBufferAttributes: sourcePixelBufferAttributes + ) + } + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraDeviceDiscoverer.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraDeviceDiscoverer.swift new file mode 100644 index 0000000..e70ed6c --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraDeviceDiscoverer.swift @@ -0,0 +1,31 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +/// A protocol which abstracts the discovery of camera devices. +/// It is a thin wrapper around `AVCaptureDiscoverySession` and it exists to allow mocking in tests. +protocol CameraDeviceDiscoverer { + func discoverySession( + withDeviceTypes deviceTypes: [AVCaptureDevice.DeviceType], + mediaType: AVMediaType, + position: AVCaptureDevice.Position + ) -> [CaptureDevice] +} + +/// The default implementation of the `CameraDeviceDiscoverer` protocol. +/// It wraps a call to `AVCaptureDeviceDiscoverySession`. +class DefaultCameraDeviceDiscoverer: NSObject, CameraDeviceDiscoverer { + func discoverySession( + withDeviceTypes deviceTypes: [AVCaptureDevice.DeviceType], + mediaType: AVMediaType, + position: AVCaptureDevice.Position + ) -> [CaptureDevice] { + return AVCaptureDevice.DiscoverySession( + deviceTypes: deviceTypes, + mediaType: mediaType, + position: position + ).devices + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraImageStreamScaler.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraImageStreamScaler.swift new file mode 100644 index 0000000..4d43b6f --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraImageStreamScaler.swift @@ -0,0 +1,41 @@ +// Copyright 2026 twonly contributors. BSD-style license; see LICENSE. + +import Accelerate +import Flutter + +/// Reduces BGRA analysis frames before they cross the Flutter bridge. The +/// original pixel buffer remains available to the preview and video recorder. +enum CameraImageStreamScaler { + // The caller holds the pixel buffer's read lock for the duration of this call. + static func scaleBGRA(_ pixelBuffer: CVPixelBuffer, maximumDimension: Int) + -> PlatformCameraImagePlane? + { + guard CVPixelBufferGetPixelFormatType(pixelBuffer) == kCVPixelFormatType_32BGRA, + maximumDimension > 0, + let address = CVPixelBufferGetBaseAddress(pixelBuffer) + else { return nil } + + let width = CVPixelBufferGetWidth(pixelBuffer) + let height = CVPixelBufferGetHeight(pixelBuffer) + let longest = max(width, height) + guard longest > maximumDimension else { return nil } + let scale = Double(maximumDimension) / Double(longest) + let targetWidth = max(1, Int((Double(width) * scale).rounded())) + let targetHeight = max(1, Int((Double(height) * scale).rounded())) + let stride = targetWidth * 4 + var bytes = Data(count: stride * targetHeight) + var source = vImage_Buffer( + data: address, height: vImagePixelCount(height), width: vImagePixelCount(width), + rowBytes: CVPixelBufferGetBytesPerRow(pixelBuffer)) + let error = bytes.withUnsafeMutableBytes { buffer -> vImage_Error in + var destination = vImage_Buffer( + data: buffer.baseAddress!, height: vImagePixelCount(targetHeight), + width: vImagePixelCount(targetWidth), rowBytes: stride) + return vImageScale_ARGB8888(&source, &destination, nil, vImage_Flags(kvImageNoFlags)) + } + guard error == kvImageNoError else { return nil } + return PlatformCameraImagePlane( + bytes: FlutterStandardTypedData(bytes: bytes), bytesPerRow: Int64(stride), + width: Int64(targetWidth), height: Int64(targetHeight)) + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraPermissionManager.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraPermissionManager.swift new file mode 100644 index 0000000..733351b --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraPermissionManager.swift @@ -0,0 +1,136 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import Flutter + +/// Completion handler for camera permission requests. +typealias CameraPermissionRequestCompletionHandler = (PigeonError?) -> Void + +private enum Permission { + case camera + case audio +} + +/// Manages camera and audio permission requests. +class CameraPermissionManager: NSObject { + let permissionService: PermissionServicing + + init(permissionService: PermissionServicing) { + self.permissionService = permissionService + super.init() + } + + /// Requests camera access permission. + /// + /// If it is the first time requesting camera access, a permission dialog will show up on the + /// screen. Otherwise AVFoundation simply returns the user's previous choice, and in this case the + /// user will have to update the choice in Settings app. + /// + /// @param handler if access permission is (or was previously) granted, completion handler will be + /// called without error; Otherwise completion handler will be called with error. Handler can be + /// called on an arbitrary dispatch queue. + func requestCameraPermission( + completionHandler handler: @escaping CameraPermissionRequestCompletionHandler + ) { + requestPermission(permission: .camera, handler: handler) + } + + /// Requests audio access permission. + /// + /// If it is the first time requesting audio access, a permission dialog will show up on the + /// screen. Otherwise AVFoundation simply returns the user's previous choice, and in this case the + /// user will have to update the choice in Settings app. + /// + /// @param handler if access permission is (or was previously) granted, completion handler will be + /// called without error; Otherwise completion handler will be called with error. Handler can be + /// called on an arbitrary dispatch queue. + func requestAudioPermission( + completionHandler handler: @escaping CameraPermissionRequestCompletionHandler + ) { + requestPermission(permission: .audio, handler: handler) + } + + private func requestPermission( + permission: Permission, + handler: @escaping CameraPermissionRequestCompletionHandler + ) { + let mediaType: AVMediaType = + switch permission { + case .audio: .audio + case .camera: .video + } + + switch permissionService.authorizationStatus(for: mediaType) { + case .authorized: + handler(nil) + + case .denied: + let flutterError = + switch permission { + case .audio: + PigeonError( + code: "AudioAccessDeniedWithoutPrompt", + message: + "User has previously denied the audio access request. Go to Settings to enable audio access.", + details: nil + ) + case .camera: + PigeonError( + code: "CameraAccessDeniedWithoutPrompt", + message: + "User has previously denied the camera access request. Go to Settings to enable camera access.", + details: nil + ) + } + handler(flutterError) + + case .restricted: + let flutterError = + switch permission { + case .audio: + PigeonError( + code: "AudioAccessRestricted", + message: "Audio access is restricted.", + details: nil + ) + case .camera: + PigeonError( + code: "CameraAccessRestricted", + message: "Camera access is restricted.", + details: nil + ) + } + handler(flutterError) + + case .notDetermined: + permissionService.requestAccess(for: mediaType) { granted in + // handler can be invoked on an arbitrary dispatch queue. + if granted { + handler(nil) + } else { + let flutterError = + switch permission { + case .audio: + PigeonError( + code: "AudioAccessDenied", + message: "User denied the audio access request.", + details: nil + ) + case .camera: + PigeonError( + code: "CameraAccessDenied", + message: "User denied the camera access request.", + details: nil + ) + } + handler(flutterError) + } + } + + @unknown default: + assertionFailure("Unknown authorization status") + } + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift new file mode 100644 index 0000000..136c05f --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift @@ -0,0 +1,567 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import Flutter + +public final class CameraPlugin: NSObject, FlutterPlugin { + private let registry: FlutterTextureRegistry + private let messenger: FlutterBinaryMessenger + private let globalEventAPI: CameraGlobalEventApiProtocol + private let deviceDiscoverer: CameraDeviceDiscoverer + private let permissionManager: CameraPermissionManager + private let captureDeviceFactory: VideoCaptureDeviceFactory + private let captureSessionFactory: CaptureSessionFactory + private let captureDeviceInputFactory: CaptureDeviceInputFactory + + /// All FLTCam's state access and capture session related operations should be on run on this queue. + private let captureSessionQueue: DispatchQueue + + /// An internal camera object that manages camera's state and performs camera operations. + var camera: Camera? + + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = CameraPlugin( + registry: registrar.textures(), + messenger: registrar.messenger(), + globalAPI: CameraGlobalEventApi(binaryMessenger: registrar.messenger()), + deviceDiscoverer: DefaultCameraDeviceDiscoverer(), + permissionManager: CameraPermissionManager( + permissionService: DefaultPermissionService()), + deviceFactory: { name in + // TODO(RobertOdrowaz) Implement better error handling and remove non-null assertion + AVCaptureDevice(uniqueID: name)! + }, + captureSessionFactory: { AVCaptureSession() }, + captureDeviceInputFactory: DefaultCaptureDeviceInputFactory(), + captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue") + ) + + CameraApiSetup.setUp(binaryMessenger: registrar.messenger(), api: instance) + } + + init( + registry: FlutterTextureRegistry, + messenger: FlutterBinaryMessenger, + globalAPI: CameraGlobalEventApiProtocol, + deviceDiscoverer: CameraDeviceDiscoverer, + permissionManager: CameraPermissionManager, + deviceFactory: @escaping VideoCaptureDeviceFactory, + captureSessionFactory: @escaping CaptureSessionFactory, + captureDeviceInputFactory: CaptureDeviceInputFactory, + captureSessionQueue: DispatchQueue + ) { + self.registry = registry + self.messenger = messenger + self.globalEventAPI = globalAPI + self.deviceDiscoverer = deviceDiscoverer + self.permissionManager = permissionManager + self.captureDeviceFactory = deviceFactory + self.captureSessionFactory = captureSessionFactory + self.captureDeviceInputFactory = captureDeviceInputFactory + self.captureSessionQueue = captureSessionQueue + + super.init() + + captureSessionQueue.setSpecific( + key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue) + + UIDevice.current.beginGeneratingDeviceOrientationNotifications() + NotificationCenter.default.addObserver( + forName: UIDevice.orientationDidChangeNotification, + object: UIDevice.current, + queue: .main + ) { [weak self] notification in + self?.orientationChanged(notification) + } + } + + public func detachFromEngine(for registrar: FlutterPluginRegistrar) { + UIDevice.current.endGeneratingDeviceOrientationNotifications() + } + + private static func pigeonErrorFromNSError(_ error: NSError) -> PigeonError { + return PigeonError( + code: "Error \(error.code)", + message: error.localizedDescription, + details: error.domain) + } + + func orientationChanged(_ notification: Notification) { + guard let device = notification.object as? UIDevice else { return } + let orientation = device.orientation + + if orientation == .faceUp || orientation == .faceDown { + // Do not change when oriented flat. + return + } + + self.captureSessionQueue.async { [weak self] in + guard let strongSelf = self else { return } + // `Camera.deviceOrientation` must be set on capture session queue. + strongSelf.camera?.deviceOrientation = orientation + // `CameraPlugin.sendDeviceOrientation` can be called on any queue. + strongSelf.sendDeviceOrientation(orientation) + } + } + + func sendDeviceOrientation(_ orientation: UIDeviceOrientation) { + DispatchQueue.main.async { [weak self] in + self?.globalEventAPI.deviceOrientationChanged( + orientation: getPigeonDeviceOrientation(for: orientation) + ) { _ in + // Ignore errors; this is essentially a broadcast stream, and + // it's fine if the other end doesn't receive the message + // (e.g., if it doesn't currently have a listener set up). + } + } + } +} + +extension CameraPlugin: CameraApi { + + func getAvailableCameras( + completion: @escaping (Result<[PlatformCameraDescription], any Error>) -> Void + ) { + captureSessionQueue.async { [weak self] in + guard let strongSelf = self else { return } + + let discoveryDevices: [AVCaptureDevice.DeviceType] = [ + .builtInWideAngleCamera, + .builtInTelephotoCamera, + .builtInUltraWideCamera, + ] + + let devices = strongSelf.deviceDiscoverer.discoverySession( + withDeviceTypes: discoveryDevices, + mediaType: .video, + position: .unspecified) + + var reply: [PlatformCameraDescription] = [] + + for device in devices { + let lensFacing = strongSelf.platformLensDirection(for: device) + let lensType = strongSelf.platformLensType(for: device) + let cameraDescription = PlatformCameraDescription( + name: device.uniqueID, + lensDirection: lensFacing, + lensType: lensType + ) + reply.append(cameraDescription) + } + + completion(.success(reply)) + } + } + + private func platformLensDirection(for device: CaptureDevice) -> PlatformCameraLensDirection { + switch device.position { + case .back: + return .back + case .front: + return .front + case .unspecified: + return .external + @unknown default: + return .external + } + } + + private func platformLensType(for device: CaptureDevice) -> PlatformCameraLensType { + switch device.deviceType { + case .builtInWideAngleCamera: + return .wide + case .builtInTelephotoCamera: + return .telephoto + case .builtInUltraWideCamera: + return .ultraWide + case .builtInDualWideCamera: + return .wide + default: + return .unknown + } + } + + func create( + cameraName: String, settings: PlatformMediaSettings, + completion: @escaping (Result) -> Void + ) { + // Create FLTCam only if granted camera access (and audio access if audio is enabled) + captureSessionQueue.async { [weak self] in + self?.permissionManager.requestCameraPermission { error in + guard let strongSelf = self else { return } + + if let error = error { + completion(.failure(error)) + return + } + + // Request audio permission on `create` call with `enableAudio` argument instead of the + // `prepareForVideoRecording` call. This is because `prepareForVideoRecording` call is + // optional, and used as a workaround to fix a missing frame issue on iOS. + if settings.enableAudio { + // Setup audio capture session only if granted audio access. + strongSelf.permissionManager.requestAudioPermission { [weak self] audioError in + // cannot use the outter `strongSelf` + guard let strongSelf = self else { return } + + if let audioError = audioError { + completion(.failure(audioError)) + return + } + + strongSelf.createCameraOnSessionQueue( + withName: cameraName, + settings: settings, + completion: completion) + } + } else { + strongSelf.createCameraOnSessionQueue( + withName: cameraName, + settings: settings, + completion: completion) + } + } + } + } + + func createCameraOnSessionQueue( + withName: String, + settings: PlatformMediaSettings, + completion: @escaping (Result) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.sessionQueueCreateCamera(name: withName, settings: settings, completion: completion) + } + } + + // This must be called on captureSessionQueue. It is extracted from createCameraOnSessionQueue + // to make it easier to reason about strong/weak self pointers. + private func sessionQueueCreateCamera( + name: String, + settings: PlatformMediaSettings, + completion: @escaping (Result) -> Void + ) { + let mediaSettingsAVWrapper = FLTCamMediaSettingsAVWrapper() + + let camConfiguration = CameraConfiguration( + mediaSettings: settings, + mediaSettingsWrapper: mediaSettingsAVWrapper, + captureDeviceFactory: captureDeviceFactory, + audioCaptureDeviceFactory: { AVCaptureDevice.default(for: .audio)! }, + captureSessionFactory: captureSessionFactory, + captureSessionQueue: captureSessionQueue, + captureDeviceInputFactory: captureDeviceInputFactory, + initialCameraName: name + ) + + do { + let newCamera = try DefaultCamera(configuration: camConfiguration) + + camera?.close() + camera = newCamera + + ensureToRunOnMainQueue { [weak self] in + guard let strongSelf = self else { return } + completion(.success(strongSelf.registry.register(newCamera))) + } + } catch let error as NSError { + completion(.failure(CameraPlugin.pigeonErrorFromNSError(error))) + } + } + + func initialize( + cameraId: Int64, imageFormat: PlatformImageFormatGroup, + completion: @escaping (Result) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.sessionQueueInitializeCamera( + cameraId, + withImageFormat: imageFormat, + completion: completion) + } + } + + // This must be called on captureSessionQueue. It is extracted from initializeCamera to make it + // easier to reason about strong/weak self pointers. + private func sessionQueueInitializeCamera( + _ cameraId: Int64, + withImageFormat imageFormat: PlatformImageFormatGroup, + completion: @escaping (Result) -> Void + ) { + guard let camera = camera else { return } + + camera.videoFormat = getPixelFormat(for: imageFormat) + + camera.onFrameAvailable = { [weak self] in + guard let camera = self?.camera else { return } + if !camera.isPreviewPaused { + ensureToRunOnMainQueue { + self?.registry.textureFrameAvailable(Int64(cameraId)) + } + } + } + + camera.dartAPI = CameraEventApi( + binaryMessenger: messenger, + messageChannelSuffix: "\(cameraId)" + ) + + camera.reportInitializationState() + sendDeviceOrientation(UIDevice.current.orientation) + camera.start() + completion(.success(())) + } + + func startImageStream(completion: @escaping (Result) -> Void) { + captureSessionQueue.async { [weak self] in + guard let strongSelf = self else { + completion(.success(())) + return + } + strongSelf.camera?.startImageStream(with: strongSelf.messenger, completion: completion) + } + } + + func stopImageStream(completion: @escaping (Result) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.stopImageStream() + completion(.success(())) + } + } + + func receivedImageStreamData(completion: @escaping (Result) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.receivedImageStreamData() + completion(.success(())) + } + } + + func dispose(cameraId: Int64, completion: @escaping (Result) -> Void) { + registry.unregisterTexture(Int64(cameraId)) + captureSessionQueue.async { [weak self] in + if let strongSelf = self { + strongSelf.camera?.close() + strongSelf.camera = nil + } + completion(.success(())) + } + } + + func lockCaptureOrientation( + orientation: PlatformDeviceOrientation, completion: @escaping (Result) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.camera?.lockCaptureOrientation(orientation) + completion(.success(())) + } + } + + func unlockCaptureOrientation(completion: @escaping (Result) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.unlockCaptureOrientation() + completion(.success(())) + } + } + + func takePicture(completion: @escaping (Result) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.captureToFile(completion: completion) + } + } + + func prepareForVideoRecording(completion: @escaping (Result) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.setUpCaptureSessionForAudioIfNeeded() + completion(.success(())) + } + } + + func startVideoRecording( + enableStream: Bool, completion: @escaping (Result) -> Void + ) { + captureSessionQueue.async { [weak self] in + guard let strongSelf = self else { return } + strongSelf.camera?.startVideoRecording( + completion: completion, + messengerForStreaming: enableStream ? strongSelf.messenger : nil) + } + } + + func stopVideoRecording(completion: @escaping (Result) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.stopVideoRecording(completion: completion) + } + } + + func pauseVideoRecording(completion: @escaping (Result) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.pauseVideoRecording() + completion(.success(())) + } + } + + func resumeVideoRecording(completion: @escaping (Result) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.resumeVideoRecording() + completion(.success(())) + } + } + + func setFlashMode( + mode: PlatformFlashMode, completion: @escaping (Result) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.camera?.setFlashMode(mode, withCompletion: completion) + } + } + + func setExposureMode( + mode: PlatformExposureMode, completion: @escaping (Result) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.camera?.setExposureMode(mode) + completion(.success(())) + } + } + + func setExposurePoint( + point: PlatformPoint?, completion: @escaping (Result) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.camera?.setExposurePoint(point, withCompletion: completion) + } + } + + func getMinExposureOffset(completion: @escaping (Result) -> Void) { + captureSessionQueue.async { [weak self] in + if let minOffset = self?.camera?.minimumExposureOffset { + completion(.success(minOffset)) + } else { + completion(.success(0)) + } + } + } + + func getMaxExposureOffset(completion: @escaping (Result) -> Void) { + captureSessionQueue.async { [weak self] in + if let maxOffset = self?.camera?.maximumExposureOffset { + completion(.success(maxOffset)) + } else { + completion(.success(0)) + } + } + } + + func setExposureOffset(offset: Double, completion: @escaping (Result) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.setExposureOffset(offset) + completion(.success(())) + } + } + + func setFocusMode( + mode: PlatformFocusMode, completion: @escaping (Result) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.camera?.setFocusMode(mode) + completion(.success(())) + } + } + + func setFocusPoint(point: PlatformPoint?, completion: @escaping (Result) -> Void) + { + captureSessionQueue.async { [weak self] in + self?.camera?.setFocusPoint(point, completion: completion) + } + } + + func getMinZoomLevel(completion: @escaping (Result) -> Void) { + captureSessionQueue.async { [weak self] in + if let minZoom = self?.camera?.minimumAvailableZoomFactor { + completion(.success(minZoom)) + } else { + completion(.success(0)) + } + } + } + + func getMaxZoomLevel(completion: @escaping (Result) -> Void) { + captureSessionQueue.async { [weak self] in + if let maxZoom = self?.camera?.maximumAvailableZoomFactor { + completion(.success(maxZoom)) + } else { + completion(.success(0)) + } + } + } + + func setZoomLevel(zoom: Double, completion: @escaping (Result) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.setZoomLevel(zoom, withCompletion: completion) + } + } + + func setVideoStabilizationMode( + mode: PlatformVideoStabilizationMode, completion: @escaping (Result) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.camera?.setVideoStabilizationMode(mode, withCompletion: completion) + } + } + + func isVideoStabilizationModeSupported( + mode: PlatformVideoStabilizationMode, + completion: @escaping (Result) -> Void + ) { + captureSessionQueue.async { [weak self] in + if let camera = self?.camera { + let isSupported = camera.isVideoStabilizationModeSupported(mode) + completion(.success(isSupported)) + } else { + completion(.success(false)) + } + } + } + + func pausePreview(completion: @escaping (Result) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.pausePreview() + completion(.success(())) + } + } + + func resumePreview(completion: @escaping (Result) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.resumePreview() + completion(.success(())) + } + } + + func updateDescriptionWhileRecording( + cameraName: String, completion: @escaping (Result) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.camera?.setDescriptionWhileRecording(cameraName, withCompletion: completion) + } + } + + func setImageFileFormat( + format: PlatformImageFileFormat, completion: @escaping (Result) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.camera?.setImageFileFormat(format) + completion(.success(())) + } + } + + func setJpegImageQuality( + quality: Int64, completion: @escaping (Result) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.camera?.setJpegImageQuality(quality) + completion(.success(())) + } + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraProperties.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraProperties.swift new file mode 100644 index 0000000..a7dfd45 --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CameraProperties.swift @@ -0,0 +1,102 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import UIKit + +/// Gets AVCaptureFlashMode from PlatformFlashMode. +/// mode - flash mode. +func getAVCaptureFlashMode(for mode: PlatformFlashMode) -> AVCaptureDevice.FlashMode { + switch mode { + case .off: + return .off + case .auto: + return .auto + case .always: + return .on + case .torch: + assertionFailure("This mode cannot be converted, and requires custom handling.") + return .off + @unknown default: + assertionFailure("Unknown flash mode") + return .off + } +} + +/// Gets UIDeviceOrientation from its Pigeon representation. +/// orientation - the Pigeon device orientation. +func getUIDeviceOrientation( + for orientation: PlatformDeviceOrientation +) -> UIDeviceOrientation { + switch orientation { + case .portraitDown: + return .portraitUpsideDown + case .landscapeLeft: + return .landscapeLeft + case .landscapeRight: + return .landscapeRight + case .portraitUp: + return .portrait + @unknown default: + assertionFailure("Unknown device orientation") + return .portrait + } +} + +/// Gets a Pigeon representation of UIDeviceOrientation. +/// orientation - the UIDeviceOrientation. +func getPigeonDeviceOrientation( + for orientation: UIDeviceOrientation +) -> PlatformDeviceOrientation { + switch orientation { + case .portraitUpsideDown: + return .portraitDown + case .landscapeLeft: + return .landscapeLeft + case .landscapeRight: + return .landscapeRight + case .portrait: + return .portraitUp + default: + return .portraitUp + } +} + +/// Gets pixel format from its Pigeon representation. +/// imageFormat - the Pigeon image format. +func getPixelFormat(for imageFormat: PlatformImageFormatGroup) -> OSType { + switch imageFormat { + case .bgra8888: + return kCVPixelFormatType_32BGRA + case .yuv420: + return kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange + @unknown default: + assertionFailure("Unknown image format") + return kCVPixelFormatType_32BGRA + } +} + +/// Gets video stabilization mode from its Pigeon representation. +/// videoStabilizationMode - the Pigeon video stabilization mode. +func getAvCaptureVideoStabilizationMode( + _ videoStabilizationMode: PlatformVideoStabilizationMode +) -> AVCaptureVideoStabilizationMode { + + switch videoStabilizationMode { + case .off: + return .off + case .standard: + return .standard + case .cinematic: + return .cinematic + case .cinematicExtended: + if #available(iOS 13.0, *) { + return .cinematicExtended + } else { + return .cinematic + } + @unknown default: + return .off + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureConnection.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureConnection.swift new file mode 100644 index 0000000..d119afb --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureConnection.swift @@ -0,0 +1,30 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +/// A protocol which is a direct passthrough to `AVCaptureConnection`. It exists to allow replacing +/// `AVCaptureConnection` in tests. +protocol CaptureConnection: NSObjectProtocol { + /// Corresponds to the `isVideoMirrored` property of `AVCaptureConnection` + var isVideoMirrored: Bool { get set } + + /// Corresponds to the `videoOrientation` property of `AVCaptureConnection` + var videoOrientation: AVCaptureVideoOrientation { get set } + + /// Corresponds to the `inputPorts` property of `AVCaptureConnection` + var inputPorts: [AVCaptureInput.Port] { get } + + /// Corresponds to the `supportsVideoMirroring` property of `AVCaptureConnection` + var isVideoMirroringSupported: Bool { get } + + /// Corresponds to the `supportsVideoOrientation` property of `AVCaptureConnection` + var isVideoOrientationSupported: Bool { get } + + /// Corresponds to the preferredVideoStabilizationMode property of `AVCaptureConnection` + var preferredVideoStabilizationMode: AVCaptureVideoStabilizationMode { get set } + +} + +extension AVCaptureConnection: CaptureConnection {} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureDevice.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureDevice.swift new file mode 100644 index 0000000..b007cb3 --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureDevice.swift @@ -0,0 +1,118 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +/// A protocol which is a direct passthrough to AVCaptureDevice. +/// It exists to allow replacing AVCaptureDevice in tests. +protocol CaptureDevice: NSObjectProtocol { + /// Underlying `AVCaptureDevice` instance. This is should not be used directly + /// in the plugin implementation code, but it exists so that other protocol default + /// implementation can pass the raw device to AVFoundation methods. + var avDevice: AVCaptureDevice { get } + + // Device identifier + var uniqueID: String { get } + + // Position/Orientation + var position: AVCaptureDevice.Position { get } + + // Lens type + var deviceType: AVCaptureDevice.DeviceType { get } + + // Format/Configuration + var flutterActiveFormat: CaptureDeviceFormat { get set } + var flutterFormats: [CaptureDeviceFormat] { get } + + // Flash/Torch + var hasFlash: Bool { get } + var hasTorch: Bool { get } + var isTorchAvailable: Bool { get } + var torchMode: AVCaptureDevice.TorchMode { get set } + func isFlashModeSupported(_ mode: AVCaptureDevice.FlashMode) -> Bool + + // Focus + var isFocusPointOfInterestSupported: Bool { get } + func isFocusModeSupported(_ mode: AVCaptureDevice.FocusMode) -> Bool + var focusMode: AVCaptureDevice.FocusMode { get set } + var focusPointOfInterest: CGPoint { get set } + + // Exposure + var isExposurePointOfInterestSupported: Bool { get } + var exposureMode: AVCaptureDevice.ExposureMode { get set } + var exposurePointOfInterest: CGPoint { get set } + var minExposureTargetBias: Float { get } + var maxExposureTargetBias: Float { get } + func setExposureTargetBias( + _ bias: Float, completionHandler handler: ((CMTime) -> Void)?) + func isExposureModeSupported(_ mode: AVCaptureDevice.ExposureMode) -> Bool + + // Zoom + var maxAvailableVideoZoomFactor: CGFloat { get } + var minAvailableVideoZoomFactor: CGFloat { get } + var videoZoomFactor: CGFloat { get set } + + // Video Stabilization + func isVideoStabilizationModeSupported(_ videoStabilizationMode: AVCaptureVideoStabilizationMode) + -> Bool + + // Camera Properties + var lensAperture: Float { get } + var exposureDuration: CMTime { get } + var iso: Float { get } + + // Configuration Lock + func lockForConfiguration() throws + func unlockForConfiguration() + + // Frame Duration + var activeVideoMinFrameDuration: CMTime { get set } + var activeVideoMaxFrameDuration: CMTime { get set } +} + +/// A protocol which is a direct passthrough to AVCaptureInput. +/// It exists to allow replacing AVCaptureInput in tests. +protocol CaptureInput: NSObjectProtocol { + /// Underlying input instance. It is exposed as raw AVCaptureInput has to be passed to some + /// AVFoundation methods. The plugin implementation code shouldn't use it though. + var avInput: AVCaptureInput { get } + + var ports: [AVCaptureInput.Port] { get } +} + +/// A protocol which wraps the creation of AVCaptureDeviceInput. +/// It exists to allow mocking instances of AVCaptureDeviceInput in tests. +protocol CaptureDeviceInputFactory: NSObjectProtocol { + func deviceInput(with device: CaptureDevice) throws -> CaptureInput +} + +extension AVCaptureDevice: CaptureDevice { + var avDevice: AVCaptureDevice { self } + + var flutterActiveFormat: CaptureDeviceFormat { + get { activeFormat } + set { activeFormat = newValue.avFormat } + } + + var flutterFormats: [CaptureDeviceFormat] { formats } + + func isVideoStabilizationModeSupported(_ videoStabilizationMode: AVCaptureVideoStabilizationMode) + -> Bool + { + return self.activeFormat.isVideoStabilizationModeSupported(videoStabilizationMode) + } + +} + +extension AVCaptureInput: CaptureInput { + var avInput: AVCaptureInput { self } +} + +/// A default implementation of CaptureDeviceInputFactory protocol which +/// wraps a call to AVCaptureInput static method `deviceInputWithDevice`. +class DefaultCaptureDeviceInputFactory: NSObject, CaptureDeviceInputFactory { + func deviceInput(with device: CaptureDevice) throws -> CaptureInput { + return try AVCaptureDeviceInput(device: device.avDevice) + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureDeviceFormat.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureDeviceFormat.swift new file mode 100644 index 0000000..6905e36 --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureDeviceFormat.swift @@ -0,0 +1,31 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +/// A protocol which is a direct passthrough to `AVFrameRateRange`. It exists to allow replacing +/// `AVFrameRateRange` in tests as it has no public initializer. +protocol FrameRateRange: NSObjectProtocol { + var minFrameRate: Float64 { get } + var maxFrameRate: Float64 { get } +} + +/// A protocol which is a direct passthrough to `AVCaptureDeviceFormat`. It exists to allow +/// replacing `AVCaptureDeviceFormat` in tests as it has no public initializer. +protocol CaptureDeviceFormat: NSObjectProtocol { + /// The underlying `AVCaptureDeviceFormat` instance. This exists so that the format + /// can be extracted when setting the active format on a device. + var avFormat: AVCaptureDevice.Format { get } + + var formatDescription: CMFormatDescription { get } + var flutterVideoSupportedFrameRateRanges: [FrameRateRange] { get } +} + +extension AVFrameRateRange: FrameRateRange {} + +extension AVCaptureDevice.Format: CaptureDeviceFormat { + var avFormat: AVCaptureDevice.Format { self } + + var flutterVideoSupportedFrameRateRanges: [FrameRateRange] { videoSupportedFrameRateRanges } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureOutput.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureOutput.swift new file mode 100644 index 0000000..5506c01 --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureOutput.swift @@ -0,0 +1,77 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +/// A protocol which is a direct passthrough to `AVCaptureOutput`. It exists to allow mocking +/// `AVCaptureOutput` in tests. +protocol CaptureOutput { + /// Returns a connection with the specified media type, or nil if no such connection exists. + func connection(with mediaType: AVMediaType) -> CaptureConnection? +} + +/// A protocol which is a direct passthrough to `AVCaptureVideoDataOutput`. It exists to allow +/// mocking `AVCaptureVideoDataOutput` in tests. +protocol CaptureVideoDataOutput: CaptureOutput { + /// The underlying instance of `AVCaptureVideoDataOutput`. + var avOutput: AVCaptureVideoDataOutput { get } + + /// Corresponds to the `alwaysDiscardsLateVideoFrames` property of `AVCaptureVideoDataOutput` + var alwaysDiscardsLateVideoFrames: Bool { get set } + + /// Corresponds to the `availableVideoPixelFormatTypes` property of `AVCaptureVideoDataOutput` + var availableVideoPixelFormatTypes: [FourCharCode] { get } + + /// Corresponds to the `videoSettings` property of `AVCaptureVideoDataOutput` + var videoSettings: [String: Any]! { get set } + + /// Corresponds to the `setSampleBufferDelegate` method of `AVCaptureVideoDataOutput` + func setSampleBufferDelegate( + _ sampleBufferDelegate: AVCaptureVideoDataOutputSampleBufferDelegate?, + queue sampleBufferCallbackQueue: DispatchQueue? + ) +} + +extension AVCaptureVideoDataOutput: CaptureVideoDataOutput { + var avOutput: AVCaptureVideoDataOutput { + return self + } + + func connection(with mediaType: AVMediaType) -> CaptureConnection? { + let connection: AVCaptureConnection? = connection(with: mediaType) + return connection + } +} + +/// A protocol which is a direct passthrough to `AVCapturePhotoOutput`. It exists to allow mocking +/// `AVCapturePhotoOutput` in tests. +protocol CapturePhotoOutput: CaptureOutput { + /// The underlying instance of `AVCapturePhotoOutput`. + var avOutput: AVCapturePhotoOutput { get } + + /// Corresponds to the `availablePhotoCodecTypes` property of `AVCapturePhotoOutput` + var availablePhotoCodecTypes: [AVVideoCodecType] { get } + + /// Corresponds to the `isHighResolutionCaptureEnabled` property of `AVCapturePhotoOutput` + var isHighResolutionCaptureEnabled: Bool { get set } + + /// Corresponds to the `supportedFlashModes` property of `AVCapturePhotoOutput` + var supportedFlashModes: [AVCaptureDevice.FlashMode] { get } + + /// Corresponds to the `capturePhotoWithSettings` method of `AVCapturePhotoOutput` + func capturePhoto(with settings: AVCapturePhotoSettings, delegate: AVCapturePhotoCaptureDelegate) +} + +/// Make AVCapturePhotoOutput conform to FLTCapturePhotoOutput protocol directly +extension AVCapturePhotoOutput: CapturePhotoOutput { + var avOutput: AVCapturePhotoOutput { + return self + } + + func connection(with mediaType: AVMediaType) -> CaptureConnection? { + // Explicit type is required to access the underlying AVCapturePhotoOutput.connection method + let connection: AVCaptureConnection? = connection(with: mediaType) + return connection + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureSession.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureSession.swift new file mode 100644 index 0000000..b93d2c9 --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/CaptureSession.swift @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +/// A protocol which is a direct passthrough to AVCaptureSession. +/// It exists to allow replacing AVCaptureSession in tests. +protocol CaptureSession: NSObjectProtocol { + var sessionPreset: AVCaptureSession.Preset { get set } + var inputs: [AVCaptureInput] { get } + var outputs: [AVCaptureOutput] { get } + var automaticallyConfiguresApplicationAudioSession: Bool { get set } + var isRunning: Bool { get } + + func beginConfiguration() + func commitConfiguration() + func startRunning() + func stopRunning() + func canSetSessionPreset(_ preset: AVCaptureSession.Preset) -> Bool + func addInputWithNoConnections(_ input: CaptureInput) + func addOutputWithNoConnections(_ output: AVCaptureOutput) + func addConnection(_ connection: AVCaptureConnection) + func addInput(_ input: CaptureInput) + func addOutput(_ output: AVCaptureOutput) + func removeInput(_ input: CaptureInput) + func removeOutput(_ output: AVCaptureOutput) + func canAddInput(_ input: CaptureInput) -> Bool + func canAddOutput(_ output: AVCaptureOutput) -> Bool + func canAddConnection(_ connection: AVCaptureConnection) -> Bool +} + +extension AVCaptureSession: CaptureSession { + func addInputWithNoConnections(_ input: CaptureInput) { + addInputWithNoConnections(input.avInput) + } + + func addInput(_ input: CaptureInput) { + addInput(input.avInput) + } + + func removeInput(_ input: CaptureInput) { + removeInput(input.avInput) + } + + func canAddInput(_ input: CaptureInput) -> Bool { + canAddInput(input.avInput) + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift new file mode 100644 index 0000000..55a915d --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -0,0 +1,1544 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import CoreMotion +import Flutter + +final class DefaultCamera: NSObject, Camera { + var dartAPI: CameraEventApi? + var onFrameAvailable: (() -> Void)? + + var videoFormat: FourCharCode = kCVPixelFormatType_32BGRA { + didSet { + captureVideoOutput.videoSettings = [ + kCVPixelBufferPixelFormatTypeKey as String: videoFormat + ] + } + } + + private(set) var isPreviewPaused = false + + var minimumExposureOffset: CGFloat { CGFloat(captureDevice.minExposureTargetBias) } + var maximumExposureOffset: CGFloat { CGFloat(captureDevice.maxExposureTargetBias) } + var minimumAvailableZoomFactor: CGFloat { captureDevice.minAvailableVideoZoomFactor } + var maximumAvailableZoomFactor: CGFloat { captureDevice.maxAvailableVideoZoomFactor } + + /// The queue on which `latestPixelBuffer` property is accessed. + /// To avoid unnecessary contention, do not access `latestPixelBuffer` on the `captureSessionQueue`. + private let pixelBufferSynchronizationQueue = DispatchQueue( + label: "io.flutter.camera.pixelBufferSynchronizationQueue") + + /// The queue on which captured photos (not videos) are written to disk. + /// Videos are written to disk by `videoAdaptor` on an internal queue managed by AVFoundation. + private let photoIOQueue = DispatchQueue(label: "io.flutter.camera.photoIOQueue") + + /// All DefaultCamera's state access and capture session related operations should be run on this queue. + private let captureSessionQueue: DispatchQueue + + private let mediaSettings: PlatformMediaSettings + private var framesPerSecond: Double? + private let mediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper + + let videoCaptureSession: CaptureSession + let audioCaptureSession: CaptureSession + + /// A wrapper for AVCaptureDevice creation to allow for dependency injection in tests. + private let videoCaptureDeviceFactory: VideoCaptureDeviceFactory + private let audioCaptureDeviceFactory: AudioCaptureDeviceFactory + private let captureDeviceInputFactory: CaptureDeviceInputFactory + private let assetWriterFactory: AssetWriterFactory + private let inputPixelBufferAdaptorFactory: InputPixelBufferAdaptorFactory + + /// A wrapper for CMVideoFormatDescriptionGetDimensions. + /// Allows for alternate implementations in tests. + private let videoDimensionsConverter: VideoDimensionsConverter + + private let deviceOrientationProvider: DeviceOrientationProvider + private let motionManager = CMMotionManager() + + private(set) var captureDevice: CaptureDevice + // Setter exposed for tests. + var captureVideoOutput: CaptureVideoDataOutput + // Setter exposed for tests. + var capturePhotoOutput: CapturePhotoOutput + private var captureVideoInput: CaptureInput + + private var videoWriter: AssetWriter? + private var videoWriterInput: AssetWriterInput? + private var audioWriterInput: AssetWriterInput? + private var assetWriterPixelBufferAdaptor: AssetWriterInputPixelBufferAdaptor? + private var videoAdaptor: AssetWriterInputPixelBufferAdaptor? + + /// A dictionary to retain all in-progress SavePhotoDelegates. The key of the dictionary is the + /// AVCapturePhotoSettings's uniqueID for each photo capture operation, and the value is the + /// SavePhotoDelegate that handles the result of each photo capture operation. Note that photo + /// capture operations may overlap, so FLTCam has to keep track of multiple delegates in progress, + /// instead of just a single delegate reference. + private(set) var inProgressSavePhotoDelegates = [Int64: SavePhotoDelegate]() + + private var imageStreamHandler: ImageStreamHandler? + + private var previewSize: CGSize? + var deviceOrientation: UIDeviceOrientation { + didSet { + guard deviceOrientation != oldValue else { return } + updateOrientation() + } + } + + /// Tracks the latest pixel buffer sent from AVFoundation's sample buffer delegate callback. + /// Used to deliver the latest pixel buffer to the flutter engine via the `copyPixelBuffer` API. + private var latestPixelBuffer: CVPixelBuffer? + + private var videoRecordingPath: String? + private(set) var isRecording = false + private var isRecordingPaused = false + private var isFirstVideoSample = false + private var isAudioSetup = false + /// Time of the end of the last sample. + private var lastSampleEndTime = CMTime.invalid + /// Whether the recording is disconnected. + private var isRecordingDisconnected = false + /// Represents sum of all pauses/interruptions during recording. + private var recordingTimeOffset = CMTime.invalid + /// Output to use for adjusting of recording time offset. + private var outputForOffsetAdjusting: AVCaptureOutput? + /// Time of the last appended video sample. + private var lastAppendedVideoSampleTime = CMTime.invalid + + /// True when images from the camera are being streamed. + private(set) var isStreamingImages = false + + /// Number of frames currently pending processing. + private var streamingPendingFramesCount = 0 + private var lastQRStreamTime = -Double.infinity + + /// Maximum number of frames pending processing. + /// To limit memory consumption, limit the number of frames pending processing. + /// After some testing, 4 was determined to be the best maximum value. + /// https://github.com/flutter/plugins/pull/4520#discussion_r766335637 + private var maxStreamingPendingFramesCount = 4 + + private var fileFormat = PlatformImageFileFormat.jpeg + private var imageQuality: Int64 = 100 + private var lockedCaptureOrientation = UIDeviceOrientation.unknown + private var exposureMode = PlatformExposureMode.auto + private var focusMode = PlatformFocusMode.auto + private var flashMode: PlatformFlashMode + + private static func pigeonErrorFromNSError(_ error: NSError) -> PigeonError { + return PigeonError( + code: "Error \(error.code)", + message: error.localizedDescription, + details: error.domain) + } + + private static func createConnection( + captureDevice: CaptureDevice, + videoFormat: FourCharCode, + captureDeviceInputFactory: CaptureDeviceInputFactory + ) throws -> (CaptureInput, CaptureVideoDataOutput, AVCaptureConnection) { + // Setup video capture input. + let captureVideoInput = try captureDeviceInputFactory.deviceInput(with: captureDevice) + + // Setup video capture output. + let captureVideoOutput = AVCaptureVideoDataOutput() + captureVideoOutput.videoSettings = [ + kCVPixelBufferPixelFormatTypeKey as String: videoFormat + ] + captureVideoOutput.alwaysDiscardsLateVideoFrames = true + + // Setup video capture connection. + let connection = AVCaptureConnection( + inputPorts: captureVideoInput.ports, + output: captureVideoOutput.avOutput) + + if captureDevice.position == .front { + connection.isVideoMirrored = true + } + + return (captureVideoInput, captureVideoOutput, connection) + } + + init(configuration: CameraConfiguration) throws { + captureSessionQueue = configuration.captureSessionQueue + mediaSettings = configuration.mediaSettings + mediaSettingsAVWrapper = configuration.mediaSettingsWrapper + videoCaptureSession = configuration.videoCaptureSession + audioCaptureSession = configuration.audioCaptureSession + videoCaptureDeviceFactory = configuration.videoCaptureDeviceFactory + audioCaptureDeviceFactory = configuration.audioCaptureDeviceFactory + captureDeviceInputFactory = configuration.captureDeviceInputFactory + assetWriterFactory = configuration.assetWriterFactory + inputPixelBufferAdaptorFactory = configuration.inputPixelBufferAdaptorFactory + videoDimensionsConverter = configuration.videoDimensionsConverter + deviceOrientationProvider = configuration.deviceOrientationProvider + + captureDevice = videoCaptureDeviceFactory(configuration.initialCameraName) + flashMode = captureDevice.hasFlash ? .auto : .off + + capturePhotoOutput = AVCapturePhotoOutput() + capturePhotoOutput.isHighResolutionCaptureEnabled = true + + videoCaptureSession.automaticallyConfiguresApplicationAudioSession = false + audioCaptureSession.automaticallyConfiguresApplicationAudioSession = false + + deviceOrientation = configuration.orientation + + let connection: AVCaptureConnection + (captureVideoInput, captureVideoOutput, connection) = try DefaultCamera.createConnection( + captureDevice: captureDevice, + videoFormat: videoFormat, + captureDeviceInputFactory: configuration.captureDeviceInputFactory) + + super.init() + + captureVideoOutput.setSampleBufferDelegate(self, queue: captureSessionQueue) + + videoCaptureSession.addInputWithNoConnections(captureVideoInput) + videoCaptureSession.addOutputWithNoConnections(captureVideoOutput.avOutput) + videoCaptureSession.addConnection(connection) + + videoCaptureSession.addOutput(capturePhotoOutput.avOutput) + + motionManager.startAccelerometerUpdates() + + if configuration.mediaSettings.framesPerSecond != nil { + // The frame rate can be changed only on a locked for configuration device. + try mediaSettingsAVWrapper.lockDevice(captureDevice) + defer { mediaSettingsAVWrapper.unlockDevice(captureDevice) } + + mediaSettingsAVWrapper.beginConfiguration(for: videoCaptureSession) + defer { mediaSettingsAVWrapper.commitConfiguration(for: videoCaptureSession) } + + try setCaptureSessionPreset(mediaSettings.resolutionPreset) + + (captureDevice.flutterActiveFormat, framesPerSecond) = FormatUtils.findBestFormat( + for: captureDevice, + mediaSettings: mediaSettings, + videoDimensionsConverter: videoDimensionsConverter) + + if let framesPerSecond = framesPerSecond { + // Set frame rate with 1/10 precision allowing non-integral values. + let fpsNominator = floor(framesPerSecond * 10.0) + let duration = CMTimeMake(value: 10, timescale: Int32(fpsNominator)) + + mediaSettingsAVWrapper.setMinFrameDuration(duration, on: captureDevice) + mediaSettingsAVWrapper.setMaxFrameDuration(duration, on: captureDevice) + } + } else { + // If the frame rate is not important fall to a less restrictive + // behavior (no configuration locking). + try setCaptureSessionPreset(mediaSettings.resolutionPreset) + } + + updateOrientation() + + // Handle video and audio interruptions and errors. Interruption can happen for example by + // an incoming call during video recording. Error can happen for example when recording starts + // during an incoming call. + // https://github.com/flutter/flutter/issues/151253 + for session in [videoCaptureSession, audioCaptureSession] { + NotificationCenter.default.addObserver( + self, + selector: #selector(captureSessionWasInterrupted), + name: AVCaptureSession.wasInterruptedNotification, + object: session) + + NotificationCenter.default.addObserver( + self, + selector: #selector(captureSessionRuntimeError), + name: AVCaptureSession.runtimeErrorNotification, + object: session) + } + } + + @objc private func captureSessionWasInterrupted(notification: NSNotification) { + isRecordingDisconnected = true + } + + @objc private func captureSessionRuntimeError(notification: NSNotification) { + reportErrorMessage( + "\(String(describing: notification.userInfo?[AVCaptureSessionErrorKey] as? Error))") + } + + // Possible values for presets are hard-coded in FLT interface having + // corresponding AVCaptureSessionPreset counterparts. + // If _resolutionPreset is not supported by camera there is + // fallback to lower resolution presets. + // If none can be selected there is error condition. + private func setCaptureSessionPreset( + _ resolutionPreset: PlatformResolutionPreset + ) throws { + switch resolutionPreset { + case .max: + if let bestFormat = highestResolutionFormat(forCaptureDevice: captureDevice) { + videoCaptureSession.sessionPreset = .inputPriority + do { + try captureDevice.lockForConfiguration() + // Set the best device format found and finish the device configuration. + captureDevice.flutterActiveFormat = bestFormat + captureDevice.unlockForConfiguration() + break + } + } + fallthrough + case .ultraHigh: + if videoCaptureSession.canSetSessionPreset(.hd4K3840x2160) { + videoCaptureSession.sessionPreset = .hd4K3840x2160 + break + } + if videoCaptureSession.canSetSessionPreset(.high) { + videoCaptureSession.sessionPreset = .high + break + } + fallthrough + case .veryHigh: + if videoCaptureSession.canSetSessionPreset(.hd1920x1080) { + videoCaptureSession.sessionPreset = .hd1920x1080 + break + } + fallthrough + case .high: + if videoCaptureSession.canSetSessionPreset(.hd1280x720) { + videoCaptureSession.sessionPreset = .hd1280x720 + break + } + fallthrough + case .medium: + if videoCaptureSession.canSetSessionPreset(.vga640x480) { + videoCaptureSession.sessionPreset = .vga640x480 + break + } + fallthrough + case .low: + if videoCaptureSession.canSetSessionPreset(.cif352x288) { + videoCaptureSession.sessionPreset = .cif352x288 + break + } + fallthrough + default: + if videoCaptureSession.canSetSessionPreset(.low) { + videoCaptureSession.sessionPreset = .low + } else { + throw NSError( + domain: NSCocoaErrorDomain, + code: URLError.unknown.rawValue, + userInfo: [ + NSLocalizedDescriptionKey: "No capture session available for current capture session." + ]) + } + } + + let size = videoDimensionsConverter(captureDevice.flutterActiveFormat) + previewSize = CGSize(width: CGFloat(size.width), height: CGFloat(size.height)) + audioCaptureSession.sessionPreset = videoCaptureSession.sessionPreset + } + + /// Finds the highest available non-square resolution in terms of pixel count for the given device. + /// Preferred are formats with the same subtype as current activeFormat. + private func highestResolutionFormat(forCaptureDevice captureDevice: CaptureDevice) + -> CaptureDeviceFormat? + { + let preferredSubType = CMFormatDescriptionGetMediaSubType( + captureDevice.flutterActiveFormat.formatDescription) + var bestFormat: CaptureDeviceFormat? = nil + var maxPixelCount: UInt = 0 + var isBestSubTypePreferred = false + + // These formats are compressed and lossy, and unsupported by the Flutter Engine. + let unsupportedSubTypes: [FourCharCode] = [ + 1_651_798_066 // Hex for 'btp2', or kCVPixelFormatType_96VersatileBayerPacked12 + ] + + for format in captureDevice.flutterFormats { + let subType = CMFormatDescriptionGetMediaSubType(format.formatDescription) + + // Skip formats that will crash the Flutter Engine + if unsupportedSubTypes.contains(subType) { + continue + } + + let resolution = videoDimensionsConverter(format) + let height = UInt(resolution.height) + let width = UInt(resolution.width) + + // Guard against 1:1 resolutions provided by the iPhone 17 centre stage sensor. + if height == width { + continue + } + + let pixelCount = height * width + let isSubTypePreferred = subType == preferredSubType + + if pixelCount > maxPixelCount + || (pixelCount == maxPixelCount && isSubTypePreferred && !isBestSubTypePreferred) + { + bestFormat = format + maxPixelCount = pixelCount + isBestSubTypePreferred = isSubTypePreferred + } + } + return bestFormat + } + + func setUpCaptureSessionForAudioIfNeeded() { + // Don't setup audio twice or we will lose the audio. + guard mediaSettings.enableAudio && !isAudioSetup else { return } + + let audioDevice = audioCaptureDeviceFactory() + do { + // Create a device input with the device and add it to the session. + // Setup the audio input. + let audioInput = try captureDeviceInputFactory.deviceInput(with: audioDevice) + + // Setup the audio output. + let audioOutput = AVCaptureAudioDataOutput() + + let block = { + // Set up options implicit to AVAudioSessionCategoryPlayback to avoid conflicts with other + // plugins like video_player. + DefaultCamera.upgradeAudioSessionCategory( + requestedCategory: .playAndRecord, + options: [.defaultToSpeaker, .allowBluetoothA2DP, .allowAirPlay] + ) + } + + if !Thread.isMainThread { + DispatchQueue.main.sync(execute: block) + } else { + block() + } + + if audioCaptureSession.canAddInput(audioInput) { + audioCaptureSession.addInput(audioInput) + + if audioCaptureSession.canAddOutput(audioOutput) { + audioCaptureSession.addOutput(audioOutput) + audioOutput.setSampleBufferDelegate(self, queue: captureSessionQueue) + isAudioSetup = true + } else { + reportErrorMessage("Unable to add Audio input/output to session capture") + isAudioSetup = false + } + } + } catch let error as NSError { + reportErrorMessage(error.description) + } + } + + // This function, although slightly modified, is also in video_player_avfoundation (in ObjC). + // Both need to do the same thing and run on the same thread (for example main thread). + // Configure application wide audio session manually to prevent overwriting flag + // MixWithOthers by capture session. + // Only change category if it is considered an upgrade which means it can only enable + // ability to play in silent mode or ability to record audio but never disables it, + // that could affect other plugins which depend on this global state. Only change + // category or options if there is change to prevent unnecessary lags and silence. + private static func upgradeAudioSessionCategory( + requestedCategory: AVAudioSession.Category, + options: AVAudioSession.CategoryOptions + ) { + let playCategories: Set = [.playback, .playAndRecord] + let recordCategories: Set = [.record, .playAndRecord] + let requiredCategories: Set = [ + requestedCategory, AVAudioSession.sharedInstance().category, + ] + + let requiresPlay = !requiredCategories.isDisjoint(with: playCategories) + let requiresRecord = !requiredCategories.isDisjoint(with: recordCategories) + + var finalCategory = requestedCategory + if requiresPlay && requiresRecord { + finalCategory = .playAndRecord + } else if requiresPlay { + finalCategory = .playback + } else if requiresRecord { + finalCategory = .record + } + + let finalOptions = AVAudioSession.sharedInstance().categoryOptions.union(options) + + if finalCategory == AVAudioSession.sharedInstance().category + && finalOptions == AVAudioSession.sharedInstance().categoryOptions + { + return + } + + try? AVAudioSession.sharedInstance().setCategory(finalCategory, options: finalOptions) + } + + func reportInitializationState() { + // Get all the state on the current thread, not the main thread. + let state = PlatformCameraState( + previewSize: PlatformSize( + // previewSize is set during init, so it will never be nil. + width: previewSize!.width, + height: previewSize!.height + ), + exposureMode: exposureMode, + focusMode: focusMode, + exposurePointSupported: captureDevice.isExposurePointOfInterestSupported, + focusPointSupported: captureDevice.isFocusPointOfInterestSupported + ) + + ensureToRunOnMainQueue { [weak self] in + self?.dartAPI?.initialized(initialState: state) { _ in + // Ignore any errors, as this is just an event broadcast. + } + } + } + + func receivedImageStreamData() { + streamingPendingFramesCount -= 1 + } + + func start() { + videoCaptureSession.startRunning() + audioCaptureSession.startRunning() + } + + func stop() { + videoCaptureSession.stopRunning() + audioCaptureSession.stopRunning() + } + + func startVideoRecording( + completion: @escaping (Result) -> Void, + messengerForStreaming messenger: FlutterBinaryMessenger? + ) { + guard !isRecording else { + completion( + .failure( + PigeonError( + code: "Error", + message: "Video is already recording", + details: nil))) + return + } + + if let messenger = messenger { + startImageStream(with: messenger) { [weak self] error in + self?.setUpVideoRecording(completion: completion) + } + return + } + + setUpVideoRecording(completion: completion) + } + + /// Main logic to setup the video recording. + private func setUpVideoRecording(completion: @escaping (Result) -> Void) { + let videoRecordingPath: String + do { + videoRecordingPath = try getTemporaryFilePath( + withExtension: "mp4", + subfolder: "videos", + prefix: "REC_") + self.videoRecordingPath = videoRecordingPath + } catch let error as NSError { + completion(.failure(DefaultCamera.pigeonErrorFromNSError(error))) + return + } + + guard setupWriter(forPath: videoRecordingPath) else { + completion( + .failure( + PigeonError( + code: "IOError", + message: "Setup Writer Failed", + details: nil))) + return + } + + // startWriting should not be called in didOutputSampleBuffer where it can cause state + // in which isRecording is true but videoWriter.status is .unknown + // in stopVideoRecording if it is called after startVideoRecording but before + // didOutputSampleBuffer had chance to call startWriting and lag at start of video + // https://github.com/flutter/flutter/issues/132016 + // https://github.com/flutter/flutter/issues/151319 + guard let videoWriter = videoWriter, videoWriter.startWriting() else { + completion( + .failure( + PigeonError( + code: "IOError", + message: "AVAssetWriter failed to start writing", + details: videoWriter?.error?.localizedDescription))) + return + } + isFirstVideoSample = true + isRecording = true + isRecordingPaused = false + isRecordingDisconnected = false + recordingTimeOffset = CMTime.zero + outputForOffsetAdjusting = captureVideoOutput.avOutput + lastAppendedVideoSampleTime = CMTime.negativeInfinity + completion(.success(())) + } + + private func setupWriter(forPath path: String) -> Bool { + setUpCaptureSessionForAudioIfNeeded() + + let videoWriter: AssetWriter + + do { + videoWriter = try assetWriterFactory(URL(fileURLWithPath: path), .mp4) + self.videoWriter = videoWriter + } catch let error as NSError { + reportErrorMessage(error.description) + return false + } + + var videoSettings = mediaSettingsAVWrapper.recommendedVideoSettingsForAssetWriter( + withFileType: + AVFileType.mp4, + for: captureVideoOutput + ) + + if mediaSettings.videoBitrate != nil || framesPerSecond != nil { + var compressionProperties: [String: Any] = [:] + + if let videoBitrate = mediaSettings.videoBitrate { + compressionProperties[AVVideoAverageBitRateKey] = Int(videoBitrate) + } + + if let framesPerSecond = framesPerSecond { + compressionProperties[AVVideoExpectedSourceFrameRateKey] = framesPerSecond + } + + videoSettings?[AVVideoCompressionPropertiesKey] = compressionProperties + } + + let videoWriterInput = mediaSettingsAVWrapper.assetWriterVideoInput( + withOutputSettings: videoSettings) + self.videoWriterInput = videoWriterInput + + let sourcePixelBufferAttributes: [String: Any] = [ + kCVPixelBufferPixelFormatTypeKey as String: videoFormat + ] + + videoAdaptor = inputPixelBufferAdaptorFactory(videoWriterInput, sourcePixelBufferAttributes) + + videoWriterInput.expectsMediaDataInRealTime = true + + // Add the audio input + if mediaSettings.enableAudio { + var audioChannelLayout = AudioChannelLayout() + audioChannelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Mono + + let audioChannelLayoutData = withUnsafeBytes(of: &audioChannelLayout) { Data($0) } + + var audioSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVSampleRateKey: 44100.0, + AVNumberOfChannelsKey: 1, + AVChannelLayoutKey: audioChannelLayoutData, + ] + + if let audioBitrate = mediaSettings.audioBitrate { + audioSettings[AVEncoderBitRateKey] = Int(audioBitrate) + } + + let newAudioWriterInput = mediaSettingsAVWrapper.assetWriterAudioInput( + withOutputSettings: audioSettings) + newAudioWriterInput.expectsMediaDataInRealTime = true + mediaSettingsAVWrapper.addInput(newAudioWriterInput, to: videoWriter) + self.audioWriterInput = newAudioWriterInput + } + + if flashMode == .torch { + try? captureDevice.lockForConfiguration() + captureDevice.torchMode = .on + captureDevice.unlockForConfiguration() + } + + mediaSettingsAVWrapper.addInput(videoWriterInput, to: videoWriter) + + captureVideoOutput.setSampleBufferDelegate(self, queue: captureSessionQueue) + + return true + } + + func pauseVideoRecording() { + isRecordingPaused = true + isRecordingDisconnected = true + } + + func resumeVideoRecording() { + isRecordingPaused = false + } + + func stopVideoRecording(completion: @escaping (Result) -> Void) { + guard isRecording else { + let error = NSError( + domain: NSCocoaErrorDomain, + code: URLError.resourceUnavailable.rawValue, + userInfo: [NSLocalizedDescriptionKey: "Video is not recording!"] + ) + completion(.failure(DefaultCamera.pigeonErrorFromNSError(error))) + return + } + + isRecording = false + + // When `isRecording` is true `startWriting` was already called so `videoWriter.status` + // is always either `.writing` or `.failed` and `finishWriting` does not throw exceptions so + // there is no need to check `videoWriter.status` + videoWriter?.finishWriting { [weak self] in + guard let strongSelf = self else { return } + + if strongSelf.videoWriter?.status == .completed { + strongSelf.updateOrientation() + completion(.success(strongSelf.videoRecordingPath!)) + strongSelf.videoRecordingPath = nil + } else { + completion( + .failure( + PigeonError( + code: "IOError", + message: "AVAssetWriter could not finish writing!", + details: nil))) + } + } + } + + func captureToFile(completion: @escaping (Result) -> Void) { + var settings = AVCapturePhotoSettings() + + if mediaSettings.resolutionPreset == .max { + settings.isHighResolutionPhotoEnabled = true + } + + let fileExtension: String + + let isHEVCCodecAvailable = capturePhotoOutput.availablePhotoCodecTypes.contains( + .hevc) + + if fileFormat == .heif, isHEVCCodecAvailable { + settings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.hevc]) + fileExtension = "heif" + } else { + fileExtension = "jpg" + if imageQuality < 100 { + settings = AVCapturePhotoSettings(format: [ + AVVideoCodecKey: AVVideoCodecType.jpeg, + AVVideoCompressionPropertiesKey: [ + AVVideoQualityKey: CGFloat(imageQuality) / 100.0 + ], + ]) + if mediaSettings.resolutionPreset == .max { + settings.isHighResolutionPhotoEnabled = true + } + } + } + + if flashMode != .torch { + settings.flashMode = getAVCaptureFlashMode(for: flashMode) + } + + let path: String + do { + path = try getTemporaryFilePath( + withExtension: fileExtension, + subfolder: "pictures", + prefix: "CAP_") + } catch let error as NSError { + completion(.failure(DefaultCamera.pigeonErrorFromNSError(error))) + return + } + + let savePhotoDelegate = SavePhotoDelegate( + path: path, + ioQueue: photoIOQueue, + completionHandler: { [weak self] path, error in + guard let strongSelf = self else { return } + + strongSelf.captureSessionQueue.async { [weak self] in + self?.inProgressSavePhotoDelegates.removeValue(forKey: settings.uniqueID) + } + + if let error = error { + completion(.failure(DefaultCamera.pigeonErrorFromNSError(error as NSError))) + } else { + assert(path != nil, "Path must not be nil if no error.") + completion(.success(path!)) + } + } + ) + + assert( + DispatchQueue.getSpecific(key: captureSessionQueueSpecificKey) + == captureSessionQueueSpecificValue, + "save photo delegate references must be updated on the capture session queue") + inProgressSavePhotoDelegates[settings.uniqueID] = savePhotoDelegate + capturePhotoOutput.capturePhoto(with: settings, delegate: savePhotoDelegate) + } + + private func getTemporaryFilePath( + withExtension ext: String, + subfolder: String, + prefix: String + ) throws -> String { + let temporaryDirectory = FileManager.default.temporaryDirectory + + let fileDirectory = temporaryDirectory.appendingPathComponent("camera").appendingPathComponent( + subfolder) + let fileName = prefix + UUID().uuidString + let file = fileDirectory.appendingPathComponent(fileName).appendingPathExtension(ext).path + + let fileManager = FileManager.default + if !fileManager.fileExists(atPath: fileDirectory.path) { + try fileManager.createDirectory( + at: fileDirectory, + withIntermediateDirectories: true, + attributes: nil) + } + + return file + } + + private func updateOrientation() { + guard !isRecording else { return } + + let orientation = + (lockedCaptureOrientation != .unknown) + ? lockedCaptureOrientation + : deviceOrientation + + updateOrientation(orientation, forCaptureOutput: capturePhotoOutput) + updateOrientation(orientation, forCaptureOutput: captureVideoOutput) + } + + private func updateOrientation( + _ orientation: UIDeviceOrientation, forCaptureOutput captureOutput: CaptureOutput + ) { + if let connection = captureOutput.connection(with: .video), + connection.isVideoOrientationSupported + { + connection.videoOrientation = videoOrientation(forDeviceOrientation: orientation) + } + } + + private func videoOrientation(forDeviceOrientation deviceOrientation: UIDeviceOrientation) + -> AVCaptureVideoOrientation + { + switch deviceOrientation { + case .portrait: + return .portrait + case .landscapeLeft: + return .landscapeRight + case .landscapeRight: + return .landscapeLeft + case .portraitUpsideDown: + return .portraitUpsideDown + default: + return .portrait + } + } + + func lockCaptureOrientation(_ pigeonOrientation: PlatformDeviceOrientation) { + let orientation = getUIDeviceOrientation(for: pigeonOrientation) + if lockedCaptureOrientation != orientation { + lockedCaptureOrientation = orientation + updateOrientation() + } + } + + func unlockCaptureOrientation() { + lockedCaptureOrientation = .unknown + updateOrientation() + } + + func setImageFileFormat(_ fileFormat: PlatformImageFileFormat) { + self.fileFormat = fileFormat + } + + func setJpegImageQuality(_ quality: Int64) { + self.imageQuality = quality + } + + func setExposureMode(_ mode: PlatformExposureMode) { + exposureMode = mode + applyExposureMode() + } + + private func applyExposureMode() { + try? captureDevice.lockForConfiguration() + switch exposureMode { + case .locked: + // AVCaptureExposureMode.autoExpose automatically adjusts the exposure one time, and then locks exposure for the device + captureDevice.exposureMode = .autoExpose + case .auto: + if captureDevice.isExposureModeSupported(.continuousAutoExposure) { + captureDevice.exposureMode = .continuousAutoExposure + } else { + captureDevice.exposureMode = .autoExpose + } + @unknown default: + assertionFailure("Unknown exposure mode") + } + captureDevice.unlockForConfiguration() + } + + func setExposureOffset(_ offset: Double) { + try? captureDevice.lockForConfiguration() + captureDevice.setExposureTargetBias(Float(offset), completionHandler: nil) + captureDevice.unlockForConfiguration() + } + + func setExposurePoint( + _ point: PlatformPoint?, withCompletion completion: @escaping (Result) -> Void + ) { + guard captureDevice.isExposurePointOfInterestSupported else { + completion( + .failure( + PigeonError( + code: "setExposurePointFailed", + message: "Device does not have exposure point capabilities", + details: nil))) + return + } + + let orientation = UIDevice.current.orientation + try? captureDevice.lockForConfiguration() + // A nil point resets to the center. + let exposurePoint = cgPoint( + for: point ?? PlatformPoint(x: 0.5, y: 0.5), withOrientation: orientation) + captureDevice.exposurePointOfInterest = exposurePoint + captureDevice.unlockForConfiguration() + // Retrigger auto exposure + applyExposureMode() + completion(.success(())) + } + + func setFocusMode(_ mode: PlatformFocusMode) { + focusMode = mode + applyFocusMode() + } + + func setFocusPoint( + _ point: PlatformPoint?, completion: @escaping (Result) -> Void + ) { + guard captureDevice.isFocusPointOfInterestSupported else { + completion( + .failure( + PigeonError( + code: "setFocusPointFailed", + message: "Device does not have focus point capabilities", + details: nil))) + return + } + + let orientation = deviceOrientationProvider.orientation + try? captureDevice.lockForConfiguration() + // A nil point resets to the center. + captureDevice.focusPointOfInterest = + cgPoint( + for: point ?? PlatformPoint(x: 0.5, y: 0.5), + withOrientation: orientation) + + captureDevice.unlockForConfiguration() + // Retrigger auto focus + applyFocusMode() + completion(.success(())) + } + + private func applyFocusMode() { + applyFocusMode(focusMode, onDevice: captureDevice) + } + + private func applyFocusMode( + _ focusMode: PlatformFocusMode, onDevice captureDevice: CaptureDevice + ) { + try? captureDevice.lockForConfiguration() + switch focusMode { + case .locked: + // AVCaptureFocusMode.autoFocus automatically adjusts the focus one time, and then locks focus + if captureDevice.isFocusModeSupported(.autoFocus) { + captureDevice.focusMode = .autoFocus + } + case .auto: + if captureDevice.isFocusModeSupported(.continuousAutoFocus) { + captureDevice.focusMode = .continuousAutoFocus + } else if captureDevice.isFocusModeSupported(.autoFocus) { + captureDevice.focusMode = .autoFocus + } + @unknown default: + assertionFailure("Unknown focus mode") + } + captureDevice.unlockForConfiguration() + } + + private func cgPoint( + for point: PlatformPoint, withOrientation orientation: UIDeviceOrientation + ) + -> CGPoint + { + var x = point.x + var y = point.y + switch orientation { + case .portrait: // 90 ccw + y = 1 - point.x + x = point.y + case .portraitUpsideDown: // 90 cw + x = 1 - point.y + y = point.x + case .landscapeRight: // 180 + x = 1 - point.x + y = 1 - point.y + case .landscapeLeft: + // No rotation required + break + default: + // No rotation required + break + } + return CGPoint(x: x, y: y) + } + + func setZoomLevel( + _ zoom: CGFloat, withCompletion completion: @escaping (Result) -> Void + ) { + if zoom < captureDevice.minAvailableVideoZoomFactor + || zoom > captureDevice.maxAvailableVideoZoomFactor + { + completion( + .failure( + PigeonError( + code: "ZOOM_ERROR", + message: + "Zoom level out of bounds (zoom level should be between \(captureDevice.minAvailableVideoZoomFactor) and \(captureDevice.maxAvailableVideoZoomFactor).", + details: nil))) + return + } + + do { + try captureDevice.lockForConfiguration() + } catch let error as NSError { + completion(.failure(DefaultCamera.pigeonErrorFromNSError(error))) + return + } + + captureDevice.videoZoomFactor = zoom + captureDevice.unlockForConfiguration() + completion(.success(())) + } + + func setVideoStabilizationMode( + _ mode: PlatformVideoStabilizationMode, + withCompletion completion: @escaping (Result) -> Void + ) { + let stabilizationMode = getAvCaptureVideoStabilizationMode(mode) + + guard captureDevice.isVideoStabilizationModeSupported(stabilizationMode) else { + completion( + .failure( + PigeonError( + code: "VIDEO_STABILIZATION_ERROR", + message: "Unavailable video stabilization mode.", + details: [ + "requested_mode": stabilizationMode.rawValue + ] + )) + ) + return + } + if let connection = captureVideoOutput.connection(with: .video) { + connection.preferredVideoStabilizationMode = stabilizationMode + } + completion(.success(())) + } + + func isVideoStabilizationModeSupported(_ mode: PlatformVideoStabilizationMode) -> Bool { + let stabilizationMode = getAvCaptureVideoStabilizationMode(mode) + return captureDevice.isVideoStabilizationModeSupported(stabilizationMode) + } + + func setFlashMode( + _ mode: PlatformFlashMode, + withCompletion completion: @escaping (Result) -> Void + ) { + switch mode { + case .torch: + guard captureDevice.hasTorch else { + completion( + .failure( + PigeonError( + code: "setFlashModeFailed", + message: "Device does not support torch mode", + details: nil)) + ) + return + } + guard captureDevice.isTorchAvailable else { + completion( + .failure( + PigeonError( + code: "setFlashModeFailed", + message: "Torch mode is currently not available", + details: nil))) + return + } + if captureDevice.torchMode != .on { + try? captureDevice.lockForConfiguration() + captureDevice.torchMode = .on + captureDevice.unlockForConfiguration() + } + case .off, .auto, .always: + guard captureDevice.hasFlash else { + completion( + .failure( + PigeonError( + code: "setFlashModeFailed", + message: "Device does not have flash capabilities", + details: nil))) + return + } + let avFlashMode = getAVCaptureFlashMode(for: mode) + guard capturePhotoOutput.supportedFlashModes.contains(avFlashMode) + else { + completion( + .failure( + PigeonError( + code: "setFlashModeFailed", + message: "Device does not support this specific flash mode", + details: nil))) + return + } + if captureDevice.torchMode != .off { + try? captureDevice.lockForConfiguration() + captureDevice.torchMode = .off + captureDevice.unlockForConfiguration() + } + @unknown default: + assertionFailure("Unknown flash mode") + } + + flashMode = mode + completion(.success(())) + } + + func pausePreview() { + isPreviewPaused = true + } + + func resumePreview() { + isPreviewPaused = false + } + + func setDescriptionWhileRecording( + _ cameraName: String, withCompletion completion: @escaping (Result) -> Void + ) { + guard isRecording else { + completion( + .failure( + PigeonError( + code: "setDescriptionWhileRecordingFailed", + message: "Device was not recording", + details: nil))) + return + } + + captureDevice = videoCaptureDeviceFactory(cameraName) + + let oldConnection = captureVideoOutput.connection(with: .video) + + // Stop video capture from the old output. + captureVideoOutput.setSampleBufferDelegate(nil, queue: nil) + + // Remove the old video capture connections. + videoCaptureSession.beginConfiguration() + videoCaptureSession.removeInput(captureVideoInput) + videoCaptureSession.removeOutput(captureVideoOutput.avOutput) + + let newConnection: AVCaptureConnection + + do { + (captureVideoInput, captureVideoOutput, newConnection) = try DefaultCamera.createConnection( + captureDevice: captureDevice, + videoFormat: videoFormat, + captureDeviceInputFactory: captureDeviceInputFactory) + + captureVideoOutput.setSampleBufferDelegate(self, queue: captureSessionQueue) + } catch { + completion( + .failure( + PigeonError( + code: "VideoError", + message: "Unable to create video connection", + details: nil))) + return + } + + // Keep the same orientation the old connections had. + if let oldConnection = oldConnection, newConnection.isVideoOrientationSupported { + newConnection.videoOrientation = oldConnection.videoOrientation + } + + // Add the new connections to the session. + if !videoCaptureSession.canAddInput(captureVideoInput) { + completion( + .failure( + PigeonError( + code: "VideoError", + message: "Unable to switch video input", + details: nil))) + } + videoCaptureSession.addInputWithNoConnections(captureVideoInput) + + if !videoCaptureSession.canAddOutput(captureVideoOutput.avOutput) { + completion( + .failure( + PigeonError( + code: "VideoError", + message: "Unable to switch video output", + details: nil))) + } + videoCaptureSession.addOutputWithNoConnections(captureVideoOutput.avOutput) + + if !videoCaptureSession.canAddConnection(newConnection) { + completion( + .failure( + PigeonError( + code: "VideoError", + message: "Unable to switch video connection", + details: nil))) + } + videoCaptureSession.addConnection(newConnection) + videoCaptureSession.commitConfiguration() + + completion(.success(())) + } + + func startImageStream( + with messenger: any FlutterBinaryMessenger, + completion: @escaping (Result) -> Void + ) { + startImageStream( + with: messenger, + imageStreamHandler: DefaultImageStreamHandler(captureSessionQueue: captureSessionQueue), + completion: completion + ) + } + + func startImageStream( + with messenger: FlutterBinaryMessenger, + imageStreamHandler: ImageStreamHandler, + completion: @escaping (Result) -> Void + ) { + if isStreamingImages { + reportErrorMessage("Images from camera are already streaming!") + completion(.success(())) + return + } + + ensureToRunOnMainQueue { [weak self] in + guard let self else { + completion(.success(())) + return + } + ImageDataStreamStreamHandler.register(with: messenger, streamHandler: imageStreamHandler) + self.imageStreamHandler = imageStreamHandler + self.captureSessionQueue.async { [weak self] in + if let self { + self.isStreamingImages = true + self.streamingPendingFramesCount = 0 + self.lastQRStreamTime = -Double.infinity + } + completion(.success(())) + } + } + } + + func stopImageStream() { + if isStreamingImages { + isStreamingImages = false + imageStreamHandler = nil + } else { + reportErrorMessage("Images from camera are not streaming!") + } + } + + func captureOutput( + _ output: AVCaptureOutput, + didOutput sampleBuffer: CMSampleBuffer, + from connection: AVCaptureConnection + ) { + if output == captureVideoOutput.avOutput { + if let newBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { + + pixelBufferSynchronizationQueue.sync { + latestPixelBuffer = newBuffer + } + + onFrameAvailable?() + } + } + + guard CMSampleBufferDataIsReady(sampleBuffer) else { + reportErrorMessage("sample buffer is not ready. Skipping sample") + return + } + + handleSampleBufferStreaming(sampleBuffer) + + if isRecording && !isRecordingPaused && videoCaptureSession.isRunning + && audioCaptureSession.isRunning + { + if videoWriter?.status == .failed, let error = videoWriter?.error { + reportErrorMessage("\(error)") + return + } + + // do not append sample buffer when readyForMoreMediaData is NO to avoid crash + // https://github.com/flutter/flutter/issues/132073 + if output == captureVideoOutput.avOutput { + if !(videoWriterInput?.isReadyForMoreMediaData ?? false) { + return + } + } else { + // ignore audio samples until the first video sample arrives to avoid black frames + // https://github.com/flutter/flutter/issues/57831 + if isFirstVideoSample || !(audioWriterInput?.isReadyForMoreMediaData ?? false) { + return + } + outputForOffsetAdjusting = output + } + + let sampleTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + + if isFirstVideoSample { + videoWriter?.startSession(atSourceTime: sampleTime) + // fix sample times not being numeric when pause/resume happens before first sample buffer + // arrives + // https://github.com/flutter/flutter/issues/132014 + isRecordingDisconnected = false + isFirstVideoSample = false + } + + var currentSampleEndTime = sampleTime + let duration = CMSampleBufferGetDuration(sampleBuffer) + if CMTIME_IS_NUMERIC(duration) { + currentSampleEndTime = CMTimeAdd(currentSampleEndTime, duration) + } + + // Use a single time offset for both video and audio to avoid desync. + // https://github.com/flutter/flutter/issues/149978 + if isRecordingDisconnected { + if output == outputForOffsetAdjusting { + let offset = CMTimeSubtract(currentSampleEndTime, lastSampleEndTime) + recordingTimeOffset = CMTimeAdd(recordingTimeOffset, offset) + lastSampleEndTime = currentSampleEndTime + isRecordingDisconnected = false + } + return + } + + if output == outputForOffsetAdjusting { + lastSampleEndTime = currentSampleEndTime + } + + if output == captureVideoOutput.avOutput { + let nextBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) + let nextSampleTime = CMTimeSubtract(sampleTime, recordingTimeOffset) + if nextSampleTime > lastAppendedVideoSampleTime { + let _ = videoAdaptor?.append(nextBuffer!, withPresentationTime: nextSampleTime) + lastAppendedVideoSampleTime = nextSampleTime + } + } else { + if recordingTimeOffset.value != 0 { + if let adjustedSampleBuffer = copySampleBufferWithAdjustedTime( + sampleBuffer, + by: recordingTimeOffset) + { + newAudioSample(adjustedSampleBuffer) + } + } else { + newAudioSample(sampleBuffer) + } + } + } + } + + private func handleSampleBufferStreaming(_ sampleBuffer: CMSampleBuffer) { + guard isStreamingImages, + let eventSink = imageStreamHandler?.eventSink, + streamingPendingFramesCount < maxStreamingPendingFramesCount + else { + return + } + + // Non-pixel buffer samples, such as audio samples, are ignored for streaming + guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { + return + } + + let isQRStream = captureDevice.position == .back + if isQRStream { + let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer).seconds + if time.isFinite { + guard time - lastQRStreamTime >= 0.1 else { return } + lastQRStreamTime = time + } + } + + streamingPendingFramesCount += 1 + + // Must lock base address before accessing the pixel data + CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) + + var imageWidth = CVPixelBufferGetWidth(pixelBuffer) + var imageHeight = CVPixelBufferGetHeight(pixelBuffer) + + var planes: [PlatformCameraImagePlane] = [] + + let isPlanar = CVPixelBufferIsPlanar(pixelBuffer) + let planeCount = isPlanar ? CVPixelBufferGetPlaneCount(pixelBuffer) : 1 + + if isQRStream, + let reduced = CameraImageStreamScaler.scaleBGRA(pixelBuffer, maximumDimension: 640) + { + planes = [reduced] + imageWidth = Int(reduced.width) + imageHeight = Int(reduced.height) + } + + for i in 0..<(planes.isEmpty ? planeCount : 0) { + let planeAddress: UnsafeMutableRawPointer? + let bytesPerRow: Int + let height: Int + let width: Int + + if isPlanar { + planeAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, i) + bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, i) + height = CVPixelBufferGetHeightOfPlane(pixelBuffer, i) + width = CVPixelBufferGetWidthOfPlane(pixelBuffer, i) + } else { + planeAddress = CVPixelBufferGetBaseAddress(pixelBuffer) + bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer) + height = CVPixelBufferGetHeight(pixelBuffer) + width = CVPixelBufferGetWidth(pixelBuffer) + } + + let length = bytesPerRow * height + let bytes = Data(bytes: planeAddress!, count: length) + + let planeBuffer = PlatformCameraImagePlane( + bytes: FlutterStandardTypedData(bytes: bytes), + bytesPerRow: Int64(bytesPerRow), + width: Int64(width), + height: Int64(height) + ) + planes.append(planeBuffer) + } + + // Lock the base address before accessing pixel data, and unlock it afterwards. + // Done accessing the `pixelBuffer` at this point. + CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) + + let imageBuffer = PlatformCameraImageData( + formatCode: Int64(videoFormat), + width: Int64(imageWidth), + height: Int64(imageHeight), + planes: planes, + lensAperture: Double(captureDevice.lensAperture), + sensorExposureTimeNanoseconds: Int64(captureDevice.exposureDuration.seconds * 1_000_000_000), + sensorSensitivity: Double(captureDevice.iso) + ) + + DispatchQueue.main.async { + eventSink.success(imageBuffer) + } + } + + private func copySampleBufferWithAdjustedTime(_ sample: CMSampleBuffer, by offset: CMTime) + -> CMSampleBuffer? + { + var count: CMItemCount = 0 + CMSampleBufferGetSampleTimingInfoArray( + sample, entryCount: 0, arrayToFill: nil, entriesNeededOut: &count) + + let timingInfo = UnsafeMutablePointer.allocate(capacity: Int(count)) + defer { timingInfo.deallocate() } + + CMSampleBufferGetSampleTimingInfoArray( + sample, entryCount: count, arrayToFill: timingInfo, entriesNeededOut: &count) + + for i in 0.. Unmanaged? { + var pixelBuffer: CVPixelBuffer? + pixelBufferSynchronizationQueue.sync { + pixelBuffer = latestPixelBuffer + latestPixelBuffer = nil + } + + if let buffer = pixelBuffer { + return Unmanaged.passRetained(buffer) + } else { + return nil + } + } + + /// Reports the given error message to the Dart side of the plugin. + /// + /// Can be called from any thread. + private func reportErrorMessage(_ errorMessage: String) { + ensureToRunOnMainQueue { [weak self] in + self?.dartAPI?.error(message: errorMessage) { _ in + // Ignore any errors, as this is just an event broadcast. + } + } + } + + deinit { + motionManager.stopAccelerometerUpdates() + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/DeviceOrientationProvider.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/DeviceOrientationProvider.swift new file mode 100644 index 0000000..2da9298 --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/DeviceOrientationProvider.swift @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import UIKit + +/// A protocol which provides the current device orientation. +/// It exists to allow replacing UIDevice in tests. +protocol DeviceOrientationProvider { + /// Returns the physical orientation of the device. + var orientation: UIDeviceOrientation { get } +} + +/// A default implementation of DeviceOrientationProvider which uses orientation +/// of the current device from UIDevice. +@objc public class DefaultDeviceOrientationProvider: NSObject, DeviceOrientationProvider { + @objc public var orientation: UIDeviceOrientation { + return UIDevice.current.orientation + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/FormatUtils.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/FormatUtils.swift new file mode 100644 index 0000000..0f351d4 --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/FormatUtils.swift @@ -0,0 +1,75 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +/// Determines the video dimensions (width and height) for a given capture device format. +/// Used in tests to mock CMVideoFormatDescriptionGetDimensions. +typealias VideoDimensionsConverter = (CaptureDeviceFormat) -> CMVideoDimensions + +enum FormatUtils { + /// Returns frame rate supported by format closest to targetFrameRate. + static private func bestFrameRate(for format: CaptureDeviceFormat, targetFrameRate: Double) + -> Double + { + var bestFrameRate: Double = 0 + var minDistance: Double = Double.greatestFiniteMagnitude + + for range in format.flutterVideoSupportedFrameRateRanges { + let frameRate = min( + max(targetFrameRate, Double(range.minFrameRate)), Double(range.maxFrameRate)) + let distance = abs(frameRate - targetFrameRate) + if distance < minDistance { + bestFrameRate = frameRate + minDistance = distance + } + } + + return bestFrameRate + } + + /// Finds format with same resolution as current activeFormat in captureDevice for which + /// bestFrameRate returned frame rate closest to mediaSettings.framesPerSecond. + /// Preferred are formats with the same subtype as current activeFormat. Returns this format + /// and frame rate which bestFrameRate returned for that format. + static func findBestFormat( + for captureDevice: CaptureDevice, + mediaSettings: PlatformMediaSettings, + videoDimensionsConverter: VideoDimensionsConverter + ) -> (format: CaptureDeviceFormat, frameRate: Double) { + let targetResolution = videoDimensionsConverter(captureDevice.flutterActiveFormat) + let targetFrameRate = Double(mediaSettings.framesPerSecond ?? 0) + let preferredSubType = CMFormatDescriptionGetMediaSubType( + captureDevice.flutterActiveFormat.formatDescription) + + var bestFormat = captureDevice.flutterActiveFormat + var resolvedBestFrameRate = bestFrameRate(for: bestFormat, targetFrameRate: targetFrameRate) + var minDistance = abs(resolvedBestFrameRate - targetFrameRate) + var isBestSubTypePreferred = true + + for format in captureDevice.flutterFormats { + let resolution = videoDimensionsConverter(format) + if resolution.width != targetResolution.width || resolution.height != targetResolution.height + { + continue + } + + let frameRate = bestFrameRate(for: format, targetFrameRate: targetFrameRate) + let distance = abs(frameRate - targetFrameRate) + let subType = CMFormatDescriptionGetMediaSubType(format.formatDescription) + let isSubTypePreferred = subType == preferredSubType + + if distance < minDistance + || (distance == minDistance && isSubTypePreferred && !isBestSubTypePreferred) + { + bestFormat = format + resolvedBestFrameRate = frameRate + minDistance = distance + isBestSubTypePreferred = isSubTypePreferred + } + } + + return (bestFormat, resolvedBestFrameRate) + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/ImageStreamHandler.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/ImageStreamHandler.swift new file mode 100644 index 0000000..3a87785 --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/ImageStreamHandler.swift @@ -0,0 +1,44 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Flutter + +/// Handles streaming of camera image data to Dart via Flutter event channels. +protocol ImageStreamHandler: ImageDataStreamStreamHandler { + /// The queue on which `eventSink` property should be accessed. + var captureSessionQueue: DispatchQueue { get } + + /// The event sink to stream camera events to Dart. + /// + /// The property should only be accessed on `captureSessionQueue`. + /// The block itself should be invoked on the main queue. + var eventSink: PigeonEventSink? { get set } +} + +/// Default implementation of ImageStreamHandler. +class DefaultImageStreamHandler: ImageDataStreamStreamHandler, ImageStreamHandler { + let captureSessionQueue: DispatchQueue + var eventSink: PigeonEventSink? + + /// Initialize an image stream handler. + /// captureSessionQueue - the queue on which the event sink should be accessed. + init(captureSessionQueue: DispatchQueue) { + self.captureSessionQueue = captureSessionQueue + super.init() + } + + override func onListen( + withArguments arguments: Any?, sink: PigeonEventSink + ) { + captureSessionQueue.async { [weak self] in + self?.eventSink = sink + } + } + + override func onCancel(withArguments arguments: Any?) { + captureSessionQueue.async { [weak self] in + self?.eventSink = nil + } + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/MediaSettingsAVWrapper.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/MediaSettingsAVWrapper.swift new file mode 100644 index 0000000..cd81296 --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/MediaSettingsAVWrapper.swift @@ -0,0 +1,99 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import CoreMedia + +/// An interface for performing media settings operations. +/// +/// xctest-expectation-checking implementation (`TestMediaSettingsAVWrapper`) of this interface can +/// be injected into `camera-avfoundation` plugin allowing to run media-settings tests without any +/// additional mocking of AVFoundation classes. +class FLTCamMediaSettingsAVWrapper { + + /// Requests exclusive access to configure device hardware properties. + /// - Parameter captureDevice: The capture device. + /// - Throws: An error if the device could not be locked for configuration. + /// - Returns: A Bool indicating whether the device was successfully locked for configuration. + func lockDevice(_ captureDevice: CaptureDevice) throws { + return try captureDevice.lockForConfiguration() + } + + /// Release exclusive control over device hardware properties. + /// - Parameter captureDevice: The capture device. + func unlockDevice(_ captureDevice: CaptureDevice) { + captureDevice.unlockForConfiguration() + } + + /// When paired with commitConfiguration, allows a client to batch multiple configuration + /// operations on a running session into atomic updates. + /// - Parameter videoCaptureSession: The video capture session. + func beginConfiguration(for videoCaptureSession: CaptureSession) { + videoCaptureSession.beginConfiguration() + } + + /// When preceded by beginConfiguration, allows a client to batch multiple configuration + /// operations on a running session into atomic updates. + /// - Parameter videoCaptureSession: The video capture session. + func commitConfiguration(for videoCaptureSession: CaptureSession) { + videoCaptureSession.commitConfiguration() + } + + /// Set receiver's current active minimum frame duration (the reciprocal of its max frame rate). + /// - Parameters: + /// - duration: The frame duration. + /// - captureDevice: The capture device + func setMinFrameDuration(_ duration: CMTime, on captureDevice: CaptureDevice) { + captureDevice.activeVideoMinFrameDuration = duration + } + + /// Set receiver's current active maximum frame duration (the reciprocal of its min frame rate). + /// - Parameters: + /// - duration: The frame duration. + /// - captureDevice: The capture device + func setMaxFrameDuration(_ duration: CMTime, on captureDevice: CaptureDevice) { + captureDevice.activeVideoMaxFrameDuration = duration + } + + /// Creates a new input of the audio media type to receive sample buffers for writing to + /// the output file. + /// - Parameter outputSettings: The settings used for encoding the audio appended to the output. + /// - Returns: An instance of `AssetWriterInput`. + func assetWriterAudioInput(withOutputSettings outputSettings: [String: Any]?) + -> AssetWriterInput + { + return AVAssetWriterInput(mediaType: .audio, outputSettings: outputSettings) + } + + /// Creates a new input of the video media type to receive sample buffers for writing to + /// the output file. + /// - Parameter outputSettings: The settings used for encoding the video appended to the output. + /// - Returns: An instance of `AssetWriterInput`. + func assetWriterVideoInput(withOutputSettings outputSettings: [String: Any]?) + -> AssetWriterInput + { + return AVAssetWriterInput(mediaType: .video, outputSettings: outputSettings) + } + + /// Adds an input to the asset writer. + /// - Parameters: + /// - writerInput: The `AssetWriterInput` object to be added. + /// - writer: The `AssetWriter` object. + func addInput(_ writerInput: AssetWriterInput, to writer: AssetWriter) { + writer.add(writerInput.avInput) + } + + /// Specifies the recommended video settings for `FLTCaptureVideoDataOutput`. + /// - Parameters: + /// - fileType: Specifies the UTI of the file type to be written (see AVMediaFormat.h for a list + /// of file format UTIs). + /// - output: The `FLTCaptureVideoDataOutput` instance. + /// - Returns: A fully populated dictionary of keys and values that are compatible with AVAssetWriter. + func recommendedVideoSettingsForAssetWriter( + withFileType fileType: AVFileType, + for output: CaptureVideoDataOutput + ) -> [String: Any]? { + return output.avOutput.recommendedVideoSettingsForAssetWriter(writingTo: fileType) + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/Messages.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/Messages.swift new file mode 100644 index 0000000..201bb78 --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/Messages.swift @@ -0,0 +1,1747 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v27.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class PigeonError: Error { + let code: String + let message: String? + let details: Sendable? + + init(code: String, message: String?, details: Sendable?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + return + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(Swift.type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func createConnectionError(withChannelName channelName: String) -> PigeonError { + return PigeonError( + code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", + details: "") +} + +enum MessagesPigeonInternal { + static func isNullish(_ value: Any?) -> Bool { + guard let innerValue = value else { + return true + } + + if case Optional.some(Optional.none) = value { + return true + } + + return innerValue is NSNull + } + static func doubleEquals(_ lhs: Double, _ rhs: Double) -> Bool { + return (lhs.isNaN && rhs.isNaN) || lhs == rhs + } + + static func doubleHash(_ value: Double, _ hasher: inout Hasher) { + if value.isNaN { + hasher.combine(0x7FF8_0000_0000_0000) + } else { + // Normalize -0.0 to 0.0 + hasher.combine(value == 0 ? 0 : value) + } + } + + static func deepEquals(_ lhs: Any?, _ rhs: Any?) -> Bool { + let cleanLhs = nilOrValue(lhs) as Any? + let cleanRhs = nilOrValue(rhs) as Any? + switch (cleanLhs, cleanRhs) { + case (nil, nil): + return true + + case (nil, _), (_, nil): + return false + + case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: + return true + + case is (Void, Void): + return true + + case (let lhsArray, let rhsArray) as ([Any?], [Any?]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !deepEquals(element, rhsArray[index]) { + return false + } + } + return true + + case (let lhsArray, let rhsArray) as ([Double], [Double]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !doubleEquals(element, rhsArray[index]) { + return false + } + } + return true + + case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard lhsDictionary.count == rhsDictionary.count else { return false } + for (lhsKey, lhsValue) in lhsDictionary { + var found = false + for (rhsKey, rhsValue) in rhsDictionary { + if deepEquals(lhsKey, rhsKey) { + if deepEquals(lhsValue, rhsValue) { + found = true + break + } else { + return false + } + } + } + if !found { return false } + } + return true + + case (let lhs as Double, let rhs as Double): + return doubleEquals(lhs, rhs) + + case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): + return lhsHashable == rhsHashable + + default: + return false + } + } + + static func deepHash(value: Any?, hasher: inout Hasher) { + let cleanValue = nilOrValue(value) as Any? + if let cleanValue = cleanValue { + if let doubleValue = cleanValue as? Double { + doubleHash(doubleValue, &hasher) + } else if let valueList = cleanValue as? [Any?] { + for item in valueList { + deepHash(value: item, hasher: &hasher) + } + } else if let valueList = cleanValue as? [Double] { + for item in valueList { + doubleHash(item, &hasher) + } + } else if let valueDict = cleanValue as? [AnyHashable: Any?] { + var result = 0 + for (key, value) in valueDict { + var entryKeyHasher = Hasher() + deepHash(value: key, hasher: &entryKeyHasher) + var entryValueHasher = Hasher() + deepHash(value: value, hasher: &entryValueHasher) + result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) + } + hasher.combine(result) + } else if let hashableValue = cleanValue as? AnyHashable { + hasher.combine(hashableValue) + } else { + hasher.combine(String(describing: cleanValue)) + } + } else { + hasher.combine(0) + } + } + +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +enum PlatformCameraLensDirection: Int, CaseIterable { + /// Front facing camera (a user looking at the screen is seen by the camera). + case front = 0 + /// Back facing camera (a user looking at the screen is not seen by the camera). + case back = 1 + /// External camera which may not be mounted to the device. + case external = 2 +} + +enum PlatformCameraLensType: Int, CaseIterable { + /// A built-in wide-angle camera device type. + case wide = 0 + /// A built-in camera device type with a longer focal length than a wide-angle camera. + case telephoto = 1 + /// A built-in camera device type with a shorter focal length than a wide-angle camera. + case ultraWide = 2 + /// Unknown camera device type. + case unknown = 3 +} + +enum PlatformDeviceOrientation: Int, CaseIterable { + case portraitUp = 0 + case landscapeLeft = 1 + case portraitDown = 2 + case landscapeRight = 3 +} + +enum PlatformExposureMode: Int, CaseIterable { + case auto = 0 + case locked = 1 +} + +enum PlatformFlashMode: Int, CaseIterable { + case off = 0 + case auto = 1 + case always = 2 + case torch = 3 +} + +enum PlatformFocusMode: Int, CaseIterable { + case auto = 0 + case locked = 1 +} + +/// Pigeon version of ImageFileFormat. +enum PlatformImageFileFormat: Int, CaseIterable { + case jpeg = 0 + case heif = 1 +} + +enum PlatformImageFormatGroup: Int, CaseIterable { + case bgra8888 = 0 + case yuv420 = 1 +} + +enum PlatformResolutionPreset: Int, CaseIterable { + case low = 0 + case medium = 1 + case high = 2 + case veryHigh = 3 + case ultraHigh = 4 + case max = 5 +} + +enum PlatformVideoStabilizationMode: Int, CaseIterable { + case off = 0 + case standard = 1 + case cinematic = 2 + case cinematicExtended = 3 +} + +/// Generated class from Pigeon that represents data sent in messages. +struct PlatformCameraDescription: Hashable, CustomStringConvertible { + /// The name of the camera device. + var name: String + /// The direction the camera is facing. + var lensDirection: PlatformCameraLensDirection + /// The type of the camera lens. + var lensType: PlatformCameraLensType + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> PlatformCameraDescription? { + let name = pigeonVar_list[0] as! String + let lensDirection = pigeonVar_list[1] as! PlatformCameraLensDirection + let lensType = pigeonVar_list[2] as! PlatformCameraLensType + + return PlatformCameraDescription( + name: name, + lensDirection: lensDirection, + lensType: lensType + ) + } + func toList() -> [Any?] { + return [ + name, + lensDirection, + lensType, + ] + } + static func == (lhs: PlatformCameraDescription, rhs: PlatformCameraDescription) -> Bool { + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return MessagesPigeonInternal.deepEquals(lhs.name, rhs.name) + && MessagesPigeonInternal.deepEquals(lhs.lensDirection, rhs.lensDirection) + && MessagesPigeonInternal.deepEquals(lhs.lensType, rhs.lensType) + } + + func hash(into hasher: inout Hasher) { + hasher.combine("PlatformCameraDescription") + MessagesPigeonInternal.deepHash(value: name, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: lensDirection, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: lensType, hasher: &hasher) + } + + public var description: String { + return + "PlatformCameraDescription(name: \(String(describing: name)), lensDirection: \(String(describing: lensDirection)), lensType: \(String(describing: lensType)))" + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct PlatformCameraState: Hashable, CustomStringConvertible { + /// The size of the preview, in pixels. + var previewSize: PlatformSize + /// The default exposure mode + var exposureMode: PlatformExposureMode + /// The default focus mode + var focusMode: PlatformFocusMode + /// Whether setting exposure points is supported. + var exposurePointSupported: Bool + /// Whether setting focus points is supported. + var focusPointSupported: Bool + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> PlatformCameraState? { + let previewSize = pigeonVar_list[0] as! PlatformSize + let exposureMode = pigeonVar_list[1] as! PlatformExposureMode + let focusMode = pigeonVar_list[2] as! PlatformFocusMode + let exposurePointSupported = pigeonVar_list[3] as! Bool + let focusPointSupported = pigeonVar_list[4] as! Bool + + return PlatformCameraState( + previewSize: previewSize, + exposureMode: exposureMode, + focusMode: focusMode, + exposurePointSupported: exposurePointSupported, + focusPointSupported: focusPointSupported + ) + } + func toList() -> [Any?] { + return [ + previewSize, + exposureMode, + focusMode, + exposurePointSupported, + focusPointSupported, + ] + } + static func == (lhs: PlatformCameraState, rhs: PlatformCameraState) -> Bool { + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return MessagesPigeonInternal.deepEquals(lhs.previewSize, rhs.previewSize) + && MessagesPigeonInternal.deepEquals(lhs.exposureMode, rhs.exposureMode) + && MessagesPigeonInternal.deepEquals(lhs.focusMode, rhs.focusMode) + && MessagesPigeonInternal.deepEquals(lhs.exposurePointSupported, rhs.exposurePointSupported) + && MessagesPigeonInternal.deepEquals(lhs.focusPointSupported, rhs.focusPointSupported) + } + + func hash(into hasher: inout Hasher) { + hasher.combine("PlatformCameraState") + MessagesPigeonInternal.deepHash(value: previewSize, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: exposureMode, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: focusMode, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: exposurePointSupported, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: focusPointSupported, hasher: &hasher) + } + + public var description: String { + return + "PlatformCameraState(previewSize: \(String(describing: previewSize)), exposureMode: \(String(describing: exposureMode)), focusMode: \(String(describing: focusMode)), exposurePointSupported: \(String(describing: exposurePointSupported)), focusPointSupported: \(String(describing: focusPointSupported)))" + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct PlatformCameraImageData: Hashable, CustomStringConvertible { + /// The FourCharCode of the image format. + var formatCode: Int64 + var width: Int64 + var height: Int64 + var planes: [PlatformCameraImagePlane] + var lensAperture: Double + var sensorExposureTimeNanoseconds: Int64 + var sensorSensitivity: Double + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> PlatformCameraImageData? { + let formatCode = pigeonVar_list[0] as! Int64 + let width = pigeonVar_list[1] as! Int64 + let height = pigeonVar_list[2] as! Int64 + let planes = pigeonVar_list[3] as! [PlatformCameraImagePlane] + let lensAperture = pigeonVar_list[4] as! Double + let sensorExposureTimeNanoseconds = pigeonVar_list[5] as! Int64 + let sensorSensitivity = pigeonVar_list[6] as! Double + + return PlatformCameraImageData( + formatCode: formatCode, + width: width, + height: height, + planes: planes, + lensAperture: lensAperture, + sensorExposureTimeNanoseconds: sensorExposureTimeNanoseconds, + sensorSensitivity: sensorSensitivity + ) + } + func toList() -> [Any?] { + return [ + formatCode, + width, + height, + planes, + lensAperture, + sensorExposureTimeNanoseconds, + sensorSensitivity, + ] + } + static func == (lhs: PlatformCameraImageData, rhs: PlatformCameraImageData) -> Bool { + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return MessagesPigeonInternal.deepEquals(lhs.formatCode, rhs.formatCode) + && MessagesPigeonInternal.deepEquals(lhs.width, rhs.width) + && MessagesPigeonInternal.deepEquals(lhs.height, rhs.height) + && MessagesPigeonInternal.deepEquals(lhs.planes, rhs.planes) + && MessagesPigeonInternal.deepEquals(lhs.lensAperture, rhs.lensAperture) + && MessagesPigeonInternal.deepEquals( + lhs.sensorExposureTimeNanoseconds, rhs.sensorExposureTimeNanoseconds) + && MessagesPigeonInternal.deepEquals(lhs.sensorSensitivity, rhs.sensorSensitivity) + } + + func hash(into hasher: inout Hasher) { + hasher.combine("PlatformCameraImageData") + MessagesPigeonInternal.deepHash(value: formatCode, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: width, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: height, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: planes, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: lensAperture, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: sensorExposureTimeNanoseconds, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: sensorSensitivity, hasher: &hasher) + } + + public var description: String { + return + "PlatformCameraImageData(formatCode: \(String(describing: formatCode)), width: \(String(describing: width)), height: \(String(describing: height)), planes: \(String(describing: planes)), lensAperture: \(String(describing: lensAperture)), sensorExposureTimeNanoseconds: \(String(describing: sensorExposureTimeNanoseconds)), sensorSensitivity: \(String(describing: sensorSensitivity)))" + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct PlatformCameraImagePlane: Hashable, CustomStringConvertible { + var bytes: FlutterStandardTypedData + var bytesPerRow: Int64 + var width: Int64 + var height: Int64 + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> PlatformCameraImagePlane? { + let bytes = pigeonVar_list[0] as! FlutterStandardTypedData + let bytesPerRow = pigeonVar_list[1] as! Int64 + let width = pigeonVar_list[2] as! Int64 + let height = pigeonVar_list[3] as! Int64 + + return PlatformCameraImagePlane( + bytes: bytes, + bytesPerRow: bytesPerRow, + width: width, + height: height + ) + } + func toList() -> [Any?] { + return [ + bytes, + bytesPerRow, + width, + height, + ] + } + static func == (lhs: PlatformCameraImagePlane, rhs: PlatformCameraImagePlane) -> Bool { + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return MessagesPigeonInternal.deepEquals(lhs.bytes, rhs.bytes) + && MessagesPigeonInternal.deepEquals(lhs.bytesPerRow, rhs.bytesPerRow) + && MessagesPigeonInternal.deepEquals(lhs.width, rhs.width) + && MessagesPigeonInternal.deepEquals(lhs.height, rhs.height) + } + + func hash(into hasher: inout Hasher) { + hasher.combine("PlatformCameraImagePlane") + MessagesPigeonInternal.deepHash(value: bytes, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: bytesPerRow, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: width, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: height, hasher: &hasher) + } + + public var description: String { + return + "PlatformCameraImagePlane(bytes: \(String(describing: bytes)), bytesPerRow: \(String(describing: bytesPerRow)), width: \(String(describing: width)), height: \(String(describing: height)))" + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct PlatformMediaSettings: Hashable, CustomStringConvertible { + var resolutionPreset: PlatformResolutionPreset + var framesPerSecond: Int64? = nil + var videoBitrate: Int64? = nil + var audioBitrate: Int64? = nil + var enableAudio: Bool + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> PlatformMediaSettings? { + let resolutionPreset = pigeonVar_list[0] as! PlatformResolutionPreset + let framesPerSecond: Int64? = nilOrValue(pigeonVar_list[1]) + let videoBitrate: Int64? = nilOrValue(pigeonVar_list[2]) + let audioBitrate: Int64? = nilOrValue(pigeonVar_list[3]) + let enableAudio = pigeonVar_list[4] as! Bool + + return PlatformMediaSettings( + resolutionPreset: resolutionPreset, + framesPerSecond: framesPerSecond, + videoBitrate: videoBitrate, + audioBitrate: audioBitrate, + enableAudio: enableAudio + ) + } + func toList() -> [Any?] { + return [ + resolutionPreset, + framesPerSecond, + videoBitrate, + audioBitrate, + enableAudio, + ] + } + static func == (lhs: PlatformMediaSettings, rhs: PlatformMediaSettings) -> Bool { + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return MessagesPigeonInternal.deepEquals(lhs.resolutionPreset, rhs.resolutionPreset) + && MessagesPigeonInternal.deepEquals(lhs.framesPerSecond, rhs.framesPerSecond) + && MessagesPigeonInternal.deepEquals(lhs.videoBitrate, rhs.videoBitrate) + && MessagesPigeonInternal.deepEquals(lhs.audioBitrate, rhs.audioBitrate) + && MessagesPigeonInternal.deepEquals(lhs.enableAudio, rhs.enableAudio) + } + + func hash(into hasher: inout Hasher) { + hasher.combine("PlatformMediaSettings") + MessagesPigeonInternal.deepHash(value: resolutionPreset, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: framesPerSecond, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: videoBitrate, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: audioBitrate, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: enableAudio, hasher: &hasher) + } + + public var description: String { + return + "PlatformMediaSettings(resolutionPreset: \(String(describing: resolutionPreset)), framesPerSecond: \(String(describing: framesPerSecond)), videoBitrate: \(String(describing: videoBitrate)), audioBitrate: \(String(describing: audioBitrate)), enableAudio: \(String(describing: enableAudio)))" + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct PlatformPoint: Hashable, CustomStringConvertible { + var x: Double + var y: Double + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> PlatformPoint? { + let x = pigeonVar_list[0] as! Double + let y = pigeonVar_list[1] as! Double + + return PlatformPoint( + x: x, + y: y + ) + } + func toList() -> [Any?] { + return [ + x, + y, + ] + } + static func == (lhs: PlatformPoint, rhs: PlatformPoint) -> Bool { + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return MessagesPigeonInternal.deepEquals(lhs.x, rhs.x) + && MessagesPigeonInternal.deepEquals(lhs.y, rhs.y) + } + + func hash(into hasher: inout Hasher) { + hasher.combine("PlatformPoint") + MessagesPigeonInternal.deepHash(value: x, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: y, hasher: &hasher) + } + + public var description: String { + return "PlatformPoint(x: \(String(describing: x)), y: \(String(describing: y)))" + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct PlatformSize: Hashable, CustomStringConvertible { + var width: Double + var height: Double + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> PlatformSize? { + let width = pigeonVar_list[0] as! Double + let height = pigeonVar_list[1] as! Double + + return PlatformSize( + width: width, + height: height + ) + } + func toList() -> [Any?] { + return [ + width, + height, + ] + } + static func == (lhs: PlatformSize, rhs: PlatformSize) -> Bool { + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return MessagesPigeonInternal.deepEquals(lhs.width, rhs.width) + && MessagesPigeonInternal.deepEquals(lhs.height, rhs.height) + } + + func hash(into hasher: inout Hasher) { + hasher.combine("PlatformSize") + MessagesPigeonInternal.deepHash(value: width, hasher: &hasher) + MessagesPigeonInternal.deepHash(value: height, hasher: &hasher) + } + + public var description: String { + return + "PlatformSize(width: \(String(describing: width)), height: \(String(describing: height)))" + } +} + +private class MessagesPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return PlatformCameraLensDirection(rawValue: enumResultAsInt) + } + return nil + case 130: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return PlatformCameraLensType(rawValue: enumResultAsInt) + } + return nil + case 131: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return PlatformDeviceOrientation(rawValue: enumResultAsInt) + } + return nil + case 132: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return PlatformExposureMode(rawValue: enumResultAsInt) + } + return nil + case 133: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return PlatformFlashMode(rawValue: enumResultAsInt) + } + return nil + case 134: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return PlatformFocusMode(rawValue: enumResultAsInt) + } + return nil + case 135: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return PlatformImageFileFormat(rawValue: enumResultAsInt) + } + return nil + case 136: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return PlatformImageFormatGroup(rawValue: enumResultAsInt) + } + return nil + case 137: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return PlatformResolutionPreset(rawValue: enumResultAsInt) + } + return nil + case 138: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return PlatformVideoStabilizationMode(rawValue: enumResultAsInt) + } + return nil + case 139: + return PlatformCameraDescription.fromList(self.readValue() as! [Any?]) + case 140: + return PlatformCameraState.fromList(self.readValue() as! [Any?]) + case 141: + return PlatformCameraImageData.fromList(self.readValue() as! [Any?]) + case 142: + return PlatformCameraImagePlane.fromList(self.readValue() as! [Any?]) + case 143: + return PlatformMediaSettings.fromList(self.readValue() as! [Any?]) + case 144: + return PlatformPoint.fromList(self.readValue() as! [Any?]) + case 145: + return PlatformSize.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class MessagesPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? PlatformCameraLensDirection { + super.writeByte(129) + super.writeValue(value.rawValue) + } else if let value = value as? PlatformCameraLensType { + super.writeByte(130) + super.writeValue(value.rawValue) + } else if let value = value as? PlatformDeviceOrientation { + super.writeByte(131) + super.writeValue(value.rawValue) + } else if let value = value as? PlatformExposureMode { + super.writeByte(132) + super.writeValue(value.rawValue) + } else if let value = value as? PlatformFlashMode { + super.writeByte(133) + super.writeValue(value.rawValue) + } else if let value = value as? PlatformFocusMode { + super.writeByte(134) + super.writeValue(value.rawValue) + } else if let value = value as? PlatformImageFileFormat { + super.writeByte(135) + super.writeValue(value.rawValue) + } else if let value = value as? PlatformImageFormatGroup { + super.writeByte(136) + super.writeValue(value.rawValue) + } else if let value = value as? PlatformResolutionPreset { + super.writeByte(137) + super.writeValue(value.rawValue) + } else if let value = value as? PlatformVideoStabilizationMode { + super.writeByte(138) + super.writeValue(value.rawValue) + } else if let value = value as? PlatformCameraDescription { + super.writeByte(139) + super.writeValue(value.toList()) + } else if let value = value as? PlatformCameraState { + super.writeByte(140) + super.writeValue(value.toList()) + } else if let value = value as? PlatformCameraImageData { + super.writeByte(141) + super.writeValue(value.toList()) + } else if let value = value as? PlatformCameraImagePlane { + super.writeByte(142) + super.writeValue(value.toList()) + } else if let value = value as? PlatformMediaSettings { + super.writeByte(143) + super.writeValue(value.toList()) + } else if let value = value as? PlatformPoint { + super.writeByte(144) + super.writeValue(value.toList()) + } else if let value = value as? PlatformSize { + super.writeByte(145) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class MessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return MessagesPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return MessagesPigeonCodecWriter(data: data) + } +} + +class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter()) +} + +var messagesPigeonMethodCodec = FlutterStandardMethodCodec( + readerWriter: MessagesPigeonCodecReaderWriter()) + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol CameraApi { + /// Returns the list of available cameras. + func getAvailableCameras( + completion: @escaping (Result<[PlatformCameraDescription], Error>) -> Void) + /// Create a new camera with the given settings, and returns its ID. + func create( + cameraName: String, settings: PlatformMediaSettings, + completion: @escaping (Result) -> Void) + /// Initializes the camera with the given ID. + func initialize( + cameraId: Int64, imageFormat: PlatformImageFormatGroup, + completion: @escaping (Result) -> Void) + /// Begins streaming frames from the camera. + func startImageStream(completion: @escaping (Result) -> Void) + /// Stops streaming frames from the camera. + func stopImageStream(completion: @escaping (Result) -> Void) + /// Called by the Dart side of the plugin when it has received the last image + /// frame sent. + /// + /// This is used to throttle sending frames across the channel. + func receivedImageStreamData(completion: @escaping (Result) -> Void) + /// Indicates that the given camera is no longer being used on the Dart side, + /// and any associated resources can be cleaned up. + func dispose(cameraId: Int64, completion: @escaping (Result) -> Void) + /// Locks the camera capture to the current device orientation. + func lockCaptureOrientation( + orientation: PlatformDeviceOrientation, completion: @escaping (Result) -> Void) + /// Unlocks camera capture orientation, allowing it to automatically adapt to + /// device orientation. + func unlockCaptureOrientation(completion: @escaping (Result) -> Void) + /// Takes a picture with the current settings, and returns the path to the + /// resulting file. + func takePicture(completion: @escaping (Result) -> Void) + /// Does any preprocessing necessary before beginning to record video. + func prepareForVideoRecording(completion: @escaping (Result) -> Void) + /// Begins recording video, optionally enabling streaming to Dart at the same + /// time. + func startVideoRecording(enableStream: Bool, completion: @escaping (Result) -> Void) + /// Stops recording video, and results the path to the resulting file. + func stopVideoRecording(completion: @escaping (Result) -> Void) + /// Pauses video recording. + func pauseVideoRecording(completion: @escaping (Result) -> Void) + /// Resumes a previously paused video recording. + func resumeVideoRecording(completion: @escaping (Result) -> Void) + /// Switches the camera to the given flash mode. + func setFlashMode(mode: PlatformFlashMode, completion: @escaping (Result) -> Void) + /// Switches the camera to the given exposure mode. + func setExposureMode( + mode: PlatformExposureMode, completion: @escaping (Result) -> Void) + /// Anchors auto-exposure to the given point in (0,1) coordinate space. + /// + /// A null value resets to the default exposure point. + func setExposurePoint(point: PlatformPoint?, completion: @escaping (Result) -> Void) + /// Returns the minimum exposure offset supported by the camera. + func getMinExposureOffset(completion: @escaping (Result) -> Void) + /// Returns the maximum exposure offset supported by the camera. + func getMaxExposureOffset(completion: @escaping (Result) -> Void) + /// Sets the exposure offset manually to the given value. + func setExposureOffset(offset: Double, completion: @escaping (Result) -> Void) + /// Switches the camera to the given focus mode. + func setFocusMode(mode: PlatformFocusMode, completion: @escaping (Result) -> Void) + /// Anchors auto-focus to the given point in (0,1) coordinate space. + /// + /// A null value resets to the default focus point. + func setFocusPoint(point: PlatformPoint?, completion: @escaping (Result) -> Void) + /// Returns the minimum zoom level supported by the camera. + func getMinZoomLevel(completion: @escaping (Result) -> Void) + /// Returns the maximum zoom level supported by the camera. + func getMaxZoomLevel(completion: @escaping (Result) -> Void) + /// Sets the zoom factor. + func setZoomLevel(zoom: Double, completion: @escaping (Result) -> Void) + /// Sets the video stabilization mode. + func setVideoStabilizationMode( + mode: PlatformVideoStabilizationMode, completion: @escaping (Result) -> Void) + /// Gets if the given video stabilization mode is supported. + func isVideoStabilizationModeSupported( + mode: PlatformVideoStabilizationMode, completion: @escaping (Result) -> Void) + /// Pauses streaming of preview frames. + func pausePreview(completion: @escaping (Result) -> Void) + /// Resumes a previously paused preview stream. + func resumePreview(completion: @escaping (Result) -> Void) + /// Changes the camera used while recording video. + /// + /// This should only be called while video recording is active. + func updateDescriptionWhileRecording( + cameraName: String, completion: @escaping (Result) -> Void) + /// Sets the file format used for taking pictures. + func setImageFileFormat( + format: PlatformImageFileFormat, completion: @escaping (Result) -> Void) + /// Sets the JPEG compression quality for still image capture. + func setJpegImageQuality(quality: Int64, completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class CameraApiSetup { + static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared } + /// Sets up an instance of `CameraApi` to handle messages through the `binaryMessenger`. + static func setUp( + binaryMessenger: FlutterBinaryMessenger, api: CameraApi?, messageChannelSuffix: String = "" + ) { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + /// Returns the list of available cameras. + let getAvailableCamerasChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.getAvailableCameras\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getAvailableCamerasChannel.setMessageHandler { _, reply in + api.getAvailableCameras { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getAvailableCamerasChannel.setMessageHandler(nil) + } + /// Create a new camera with the given settings, and returns its ID. + let createChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.create\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + createChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let cameraNameArg = args[0] as! String + let settingsArg = args[1] as! PlatformMediaSettings + api.create(cameraName: cameraNameArg, settings: settingsArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + createChannel.setMessageHandler(nil) + } + /// Initializes the camera with the given ID. + let initializeChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.initialize\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + initializeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let cameraIdArg = args[0] as! Int64 + let imageFormatArg = args[1] as! PlatformImageFormatGroup + api.initialize(cameraId: cameraIdArg, imageFormat: imageFormatArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + initializeChannel.setMessageHandler(nil) + } + /// Begins streaming frames from the camera. + let startImageStreamChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.startImageStream\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + startImageStreamChannel.setMessageHandler { _, reply in + api.startImageStream { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + startImageStreamChannel.setMessageHandler(nil) + } + /// Stops streaming frames from the camera. + let stopImageStreamChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.stopImageStream\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + stopImageStreamChannel.setMessageHandler { _, reply in + api.stopImageStream { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + stopImageStreamChannel.setMessageHandler(nil) + } + /// Called by the Dart side of the plugin when it has received the last image + /// frame sent. + /// + /// This is used to throttle sending frames across the channel. + let receivedImageStreamDataChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.camera_avfoundation.CameraApi.receivedImageStreamData\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + receivedImageStreamDataChannel.setMessageHandler { _, reply in + api.receivedImageStreamData { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + receivedImageStreamDataChannel.setMessageHandler(nil) + } + /// Indicates that the given camera is no longer being used on the Dart side, + /// and any associated resources can be cleaned up. + let disposeChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.dispose\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + disposeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let cameraIdArg = args[0] as! Int64 + api.dispose(cameraId: cameraIdArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + disposeChannel.setMessageHandler(nil) + } + /// Locks the camera capture to the current device orientation. + let lockCaptureOrientationChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.camera_avfoundation.CameraApi.lockCaptureOrientation\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + lockCaptureOrientationChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let orientationArg = args[0] as! PlatformDeviceOrientation + api.lockCaptureOrientation(orientation: orientationArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + lockCaptureOrientationChannel.setMessageHandler(nil) + } + /// Unlocks camera capture orientation, allowing it to automatically adapt to + /// device orientation. + let unlockCaptureOrientationChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.camera_avfoundation.CameraApi.unlockCaptureOrientation\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + unlockCaptureOrientationChannel.setMessageHandler { _, reply in + api.unlockCaptureOrientation { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + unlockCaptureOrientationChannel.setMessageHandler(nil) + } + /// Takes a picture with the current settings, and returns the path to the + /// resulting file. + let takePictureChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.takePicture\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + takePictureChannel.setMessageHandler { _, reply in + api.takePicture { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + takePictureChannel.setMessageHandler(nil) + } + /// Does any preprocessing necessary before beginning to record video. + let prepareForVideoRecordingChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.camera_avfoundation.CameraApi.prepareForVideoRecording\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + prepareForVideoRecordingChannel.setMessageHandler { _, reply in + api.prepareForVideoRecording { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + prepareForVideoRecordingChannel.setMessageHandler(nil) + } + /// Begins recording video, optionally enabling streaming to Dart at the same + /// time. + let startVideoRecordingChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.startVideoRecording\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + startVideoRecordingChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let enableStreamArg = args[0] as! Bool + api.startVideoRecording(enableStream: enableStreamArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + startVideoRecordingChannel.setMessageHandler(nil) + } + /// Stops recording video, and results the path to the resulting file. + let stopVideoRecordingChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.stopVideoRecording\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + stopVideoRecordingChannel.setMessageHandler { _, reply in + api.stopVideoRecording { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + stopVideoRecordingChannel.setMessageHandler(nil) + } + /// Pauses video recording. + let pauseVideoRecordingChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.pauseVideoRecording\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + pauseVideoRecordingChannel.setMessageHandler { _, reply in + api.pauseVideoRecording { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + pauseVideoRecordingChannel.setMessageHandler(nil) + } + /// Resumes a previously paused video recording. + let resumeVideoRecordingChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.resumeVideoRecording\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + resumeVideoRecordingChannel.setMessageHandler { _, reply in + api.resumeVideoRecording { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + resumeVideoRecordingChannel.setMessageHandler(nil) + } + /// Switches the camera to the given flash mode. + let setFlashModeChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.setFlashMode\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setFlashModeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let modeArg = args[0] as! PlatformFlashMode + api.setFlashMode(mode: modeArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setFlashModeChannel.setMessageHandler(nil) + } + /// Switches the camera to the given exposure mode. + let setExposureModeChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.setExposureMode\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setExposureModeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let modeArg = args[0] as! PlatformExposureMode + api.setExposureMode(mode: modeArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setExposureModeChannel.setMessageHandler(nil) + } + /// Anchors auto-exposure to the given point in (0,1) coordinate space. + /// + /// A null value resets to the default exposure point. + let setExposurePointChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.setExposurePoint\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setExposurePointChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let pointArg: PlatformPoint? = nilOrValue(args[0]) + api.setExposurePoint(point: pointArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setExposurePointChannel.setMessageHandler(nil) + } + /// Returns the minimum exposure offset supported by the camera. + let getMinExposureOffsetChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.getMinExposureOffset\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getMinExposureOffsetChannel.setMessageHandler { _, reply in + api.getMinExposureOffset { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getMinExposureOffsetChannel.setMessageHandler(nil) + } + /// Returns the maximum exposure offset supported by the camera. + let getMaxExposureOffsetChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.getMaxExposureOffset\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getMaxExposureOffsetChannel.setMessageHandler { _, reply in + api.getMaxExposureOffset { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getMaxExposureOffsetChannel.setMessageHandler(nil) + } + /// Sets the exposure offset manually to the given value. + let setExposureOffsetChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.setExposureOffset\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setExposureOffsetChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let offsetArg = args[0] as! Double + api.setExposureOffset(offset: offsetArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setExposureOffsetChannel.setMessageHandler(nil) + } + /// Switches the camera to the given focus mode. + let setFocusModeChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.setFocusMode\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setFocusModeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let modeArg = args[0] as! PlatformFocusMode + api.setFocusMode(mode: modeArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setFocusModeChannel.setMessageHandler(nil) + } + /// Anchors auto-focus to the given point in (0,1) coordinate space. + /// + /// A null value resets to the default focus point. + let setFocusPointChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.setFocusPoint\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setFocusPointChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let pointArg: PlatformPoint? = nilOrValue(args[0]) + api.setFocusPoint(point: pointArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setFocusPointChannel.setMessageHandler(nil) + } + /// Returns the minimum zoom level supported by the camera. + let getMinZoomLevelChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.getMinZoomLevel\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getMinZoomLevelChannel.setMessageHandler { _, reply in + api.getMinZoomLevel { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getMinZoomLevelChannel.setMessageHandler(nil) + } + /// Returns the maximum zoom level supported by the camera. + let getMaxZoomLevelChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.getMaxZoomLevel\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getMaxZoomLevelChannel.setMessageHandler { _, reply in + api.getMaxZoomLevel { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getMaxZoomLevelChannel.setMessageHandler(nil) + } + /// Sets the zoom factor. + let setZoomLevelChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.setZoomLevel\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setZoomLevelChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let zoomArg = args[0] as! Double + api.setZoomLevel(zoom: zoomArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setZoomLevelChannel.setMessageHandler(nil) + } + /// Sets the video stabilization mode. + let setVideoStabilizationModeChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.camera_avfoundation.CameraApi.setVideoStabilizationMode\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setVideoStabilizationModeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let modeArg = args[0] as! PlatformVideoStabilizationMode + api.setVideoStabilizationMode(mode: modeArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setVideoStabilizationModeChannel.setMessageHandler(nil) + } + /// Gets if the given video stabilization mode is supported. + let isVideoStabilizationModeSupportedChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.camera_avfoundation.CameraApi.isVideoStabilizationModeSupported\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + isVideoStabilizationModeSupportedChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let modeArg = args[0] as! PlatformVideoStabilizationMode + api.isVideoStabilizationModeSupported(mode: modeArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + isVideoStabilizationModeSupportedChannel.setMessageHandler(nil) + } + /// Pauses streaming of preview frames. + let pausePreviewChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.pausePreview\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + pausePreviewChannel.setMessageHandler { _, reply in + api.pausePreview { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + pausePreviewChannel.setMessageHandler(nil) + } + /// Resumes a previously paused preview stream. + let resumePreviewChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.resumePreview\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + resumePreviewChannel.setMessageHandler { _, reply in + api.resumePreview { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + resumePreviewChannel.setMessageHandler(nil) + } + /// Changes the camera used while recording video. + /// + /// This should only be called while video recording is active. + let updateDescriptionWhileRecordingChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.camera_avfoundation.CameraApi.updateDescriptionWhileRecording\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + updateDescriptionWhileRecordingChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let cameraNameArg = args[0] as! String + api.updateDescriptionWhileRecording(cameraName: cameraNameArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + updateDescriptionWhileRecordingChannel.setMessageHandler(nil) + } + /// Sets the file format used for taking pictures. + let setImageFileFormatChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.setImageFileFormat\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setImageFileFormatChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let formatArg = args[0] as! PlatformImageFileFormat + api.setImageFileFormat(format: formatArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setImageFileFormatChannel.setMessageHandler(nil) + } + /// Sets the JPEG compression quality for still image capture. + let setJpegImageQualityChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.setJpegImageQuality\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setJpegImageQualityChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let qualityArg = args[0] as! Int64 + api.setJpegImageQuality(quality: qualityArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setJpegImageQualityChannel.setMessageHandler(nil) + } + } +} + +private class PigeonStreamHandler: NSObject, FlutterStreamHandler { + private let wrapper: PigeonEventChannelWrapper + private var pigeonSink: PigeonEventSink? = nil + + init(wrapper: PigeonEventChannelWrapper) { + self.wrapper = wrapper + } + + func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) + -> FlutterError? + { + pigeonSink = PigeonEventSink(events) + wrapper.onListen(withArguments: arguments, sink: pigeonSink!) + return nil + } + + func onCancel(withArguments arguments: Any?) -> FlutterError? { + pigeonSink = nil + wrapper.onCancel(withArguments: arguments) + return nil + } +} + +class PigeonEventChannelWrapper { + func onListen(withArguments arguments: Any?, sink: PigeonEventSink) {} + func onCancel(withArguments arguments: Any?) {} +} + +class PigeonEventSink { + private let sink: FlutterEventSink + + init(_ sink: @escaping FlutterEventSink) { + self.sink = sink + } + + func success(_ value: ReturnType) { + sink(value) + } + + func error(code: String, message: String?, details: Any?) { + sink(FlutterError(code: code, message: message, details: details)) + } + + func endOfStream() { + sink(FlutterEndOfEventStream) + } + +} + +class ImageDataStreamStreamHandler: PigeonEventChannelWrapper { + static func register( + with messenger: FlutterBinaryMessenger, + instanceName: String = "", + streamHandler: ImageDataStreamStreamHandler + ) { + var channelName = + "dev.flutter.pigeon.camera_avfoundation.CameraImageStreamEventApi.imageDataStream" + if !instanceName.isEmpty { + channelName += ".\(instanceName)" + } + let internalStreamHandler = PigeonStreamHandler(wrapper: streamHandler) + let channel = FlutterEventChannel( + name: channelName, binaryMessenger: messenger, codec: messagesPigeonMethodCodec) + channel.setStreamHandler(internalStreamHandler) + } +} + +/// Handler for native callbacks that are not tied to a specific camera ID. +/// +/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. +protocol CameraGlobalEventApiProtocol { + /// Called when the device's physical orientation changes. + func deviceOrientationChanged( + orientation orientationArg: PlatformDeviceOrientation, + completion: @escaping (Result) -> Void) +} +class CameraGlobalEventApi: CameraGlobalEventApiProtocol { + private let binaryMessenger: FlutterBinaryMessenger + private let messageChannelSuffix: String + init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") { + self.binaryMessenger = binaryMessenger + self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + } + var codec: MessagesPigeonCodec { + return MessagesPigeonCodec.shared + } + /// Called when the device's physical orientation changes. + func deviceOrientationChanged( + orientation orientationArg: PlatformDeviceOrientation, + completion: @escaping (Result) -> Void + ) { + let channelName: String = + "dev.flutter.pigeon.camera_avfoundation.CameraGlobalEventApi.deviceOrientationChanged\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel( + name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([orientationArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } +} + +/// Handler for native callbacks that are tied to a specific camera ID. +/// +/// This is intended to be initialized with the camera ID as a suffix. +/// +/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. +protocol CameraEventApiProtocol { + /// Called when the camera is inialitized for use. + func initialized( + initialState initialStateArg: PlatformCameraState, + completion: @escaping (Result) -> Void) + /// Called when an error occurs in the camera. + /// + /// This should be used for errors that occur outside of the context of + /// handling a specific HostApi call, such as during streaming. + func error(message messageArg: String, completion: @escaping (Result) -> Void) +} +class CameraEventApi: CameraEventApiProtocol { + private let binaryMessenger: FlutterBinaryMessenger + private let messageChannelSuffix: String + init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") { + self.binaryMessenger = binaryMessenger + self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + } + var codec: MessagesPigeonCodec { + return MessagesPigeonCodec.shared + } + /// Called when the camera is inialitized for use. + func initialized( + initialState initialStateArg: PlatformCameraState, + completion: @escaping (Result) -> Void + ) { + let channelName: String = + "dev.flutter.pigeon.camera_avfoundation.CameraEventApi.initialized\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel( + name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([initialStateArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } + /// Called when an error occurs in the camera. + /// + /// This should be used for errors that occur outside of the context of + /// handling a specific HostApi call, such as during streaming. + func error(message messageArg: String, completion: @escaping (Result) -> Void) + { + let channelName: String = + "dev.flutter.pigeon.camera_avfoundation.CameraEventApi.error\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel( + name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([messageArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else { + completion(.success(())) + } + } + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/PermissionServicing.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/PermissionServicing.swift new file mode 100644 index 0000000..b0c72d9 --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/PermissionServicing.swift @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation + +/// A protocol for permission-related operations on AVCaptureDevice. +/// It exists to allow mocking permission checks in tests. +protocol PermissionServicing: NSObjectProtocol { + func authorizationStatus(for mediaType: AVMediaType) -> AVAuthorizationStatus + func requestAccess( + for mediaType: AVMediaType, + completion handler: @escaping @Sendable (Bool) -> Void + ) +} + +/// Default implementation of PermissionServicing that forwards calls to AVCaptureDevice. +class DefaultPermissionService: NSObject, PermissionServicing { + func authorizationStatus(for mediaType: AVMediaType) -> AVAuthorizationStatus { + return AVCaptureDevice.authorizationStatus(for: mediaType) + } + + func requestAccess( + for mediaType: AVMediaType, + completion handler: @escaping @Sendable (Bool) -> Void + ) { + AVCaptureDevice.requestAccess(for: mediaType, completionHandler: handler) + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/QueueUtils.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/QueueUtils.swift new file mode 100644 index 0000000..99fe225 --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/QueueUtils.swift @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Dispatch +import Foundation + +/// Queue-specific context data to be associated with the capture session queue. +let captureSessionQueueSpecificKey = DispatchSpecificKey() +let captureSessionQueueSpecificValue = "capture_session_queue" + +/// Ensures the given block to be run on the main queue. +/// If caller site is already on the main queue, the block will be run +/// synchronously. Otherwise, the block will be dispatched asynchronously to the +/// main queue. +/// block - the block to be run on the main queue. +func ensureToRunOnMainQueue(_ block: @escaping () -> Void) { + if Thread.isMainThread { + block() + } else { + DispatchQueue.main.async { + block() + } + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/Resources/PrivacyInfo.xcprivacy b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..a34b7e2 --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/SavePhotoDelegate.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/SavePhotoDelegate.swift new file mode 100644 index 0000000..3505012 --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/SavePhotoDelegate.swift @@ -0,0 +1,85 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import Flutter +import Foundation + +/// The completion handler block for save photo operations. +/// Can be called from either main queue or IO queue. +/// If success, `path` will be present and `error` will be nil. Otherwise, `path` will be nil and +/// `error` will be present. +/// path - the path for successfully saved photo file. +/// error - photo capture error or IO error. +typealias SavePhotoDelegateCompletionHandler = (String?, Error?) -> Void + +/// Delegate object that handles photo capture results. +class SavePhotoDelegate: NSObject, AVCapturePhotoCaptureDelegate { + /// The file path for the captured photo. + private let path: String + + /// The queue on which captured photos are written to disk. + private let ioQueue: DispatchQueue + + /// The completion handler block for capture and save photo operations. + let completionHandler: SavePhotoDelegateCompletionHandler + + /// The path for captured photo file. + /// Exposed for unit tests to verify the captured photo file path. + var filePath: String { + path + } + + /// Initialize a photo capture delegate. + /// path - the path for captured photo file. + /// ioQueue - the queue on which captured photos are written to disk. + /// completionHandler - The completion handler block for save photo operations. Can + /// be called from either main queue or IO queue. + init( + path: String, + ioQueue: DispatchQueue, + completionHandler: @escaping SavePhotoDelegateCompletionHandler + ) { + self.path = path + self.ioQueue = ioQueue + self.completionHandler = completionHandler + super.init() + } + + /// Handler to write captured photo data into a file. + /// - Parameters: + /// - error: The capture error + /// - photoDataProvider: A closure that provides photo data + func handlePhotoCaptureResult( + error: Error?, + photoDataProvider: @escaping () -> WritableData? + ) { + if let error = error { + completionHandler(nil, error) + return + } + + ioQueue.async { [weak self] in + guard let strongSelf = self else { return } + + do { + let data = photoDataProvider() + try data?.writeToPath(strongSelf.path, options: .atomic) + strongSelf.completionHandler(strongSelf.path, nil) + } catch { + strongSelf.completionHandler(nil, error) + } + } + } + + func photoOutput( + _ output: AVCapturePhotoOutput, + didFinishProcessingPhoto photo: AVCapturePhoto, + error: Error? + ) { + handlePhotoCaptureResult(error: error) { + photo.fileDataRepresentation() + } + } +} diff --git a/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/WritableData.swift b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/WritableData.swift new file mode 100644 index 0000000..ffc3009 --- /dev/null +++ b/camera_avfoundation/darwin/camera_avfoundation/Sources/camera_avfoundation/WritableData.swift @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Foundation + +/// A protocol which abstracts the file writing operation implemented in `Data`. +/// It exists to allow replacing `Data` in tests. +protocol WritableData { + func writeToPath( + _ path: String, + options: Data.WritingOptions + ) throws +} + +extension Data: WritableData { + func writeToPath(_ path: String, options: Data.WritingOptions) throws { + try write(to: URL(fileURLWithPath: path), options: options) + } +} diff --git a/camera_avfoundation/lib/camera_avfoundation.dart b/camera_avfoundation/lib/camera_avfoundation.dart new file mode 100644 index 0000000..e6f7340 --- /dev/null +++ b/camera_avfoundation/lib/camera_avfoundation.dart @@ -0,0 +1,5 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/avfoundation_camera.dart'; diff --git a/camera_avfoundation/lib/src/avfoundation_camera.dart b/camera_avfoundation/lib/src/avfoundation_camera.dart new file mode 100644 index 0000000..f06439f --- /dev/null +++ b/camera_avfoundation/lib/src/avfoundation_camera.dart @@ -0,0 +1,650 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:math'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_transform/stream_transform.dart'; + +import 'messages.g.dart'; +import 'type_conversion.dart'; +import 'utils.dart'; + +/// An iOS implementation of [CameraPlatform] based on AVFoundation. +class AVFoundationCamera extends CameraPlatform { + /// Creates a new AVFoundation-based [CameraPlatform] implementation instance. + AVFoundationCamera({@visibleForTesting CameraApi? api}) : _hostApi = api ?? CameraApi(); + + /// Registers this class as the default instance of [CameraPlatform]. + static void registerWith() { + CameraPlatform.instance = AVFoundationCamera(); + } + + /// Interface for calling host-side code. + final CameraApi _hostApi; + + /// The controller we need to broadcast the different events coming + /// from handleMethodCall, specific to camera events. + /// + /// It is a `broadcast` because multiple controllers will connect to + /// different stream views of this Controller. + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + @visibleForTesting + final StreamController cameraEventStreamController = + StreamController.broadcast(); + + /// The handler for device-level messages that should be rebroadcast to + /// clients as [DeviceEvent]s. + @visibleForTesting + late final HostDeviceMessageHandler hostHandler = () { + // Set up the method handler lazily. + return HostDeviceMessageHandler(); + }(); + + /// The per-camera handlers for messages that should be rebroadcast to + /// clients as [CameraEvent]s. + @visibleForTesting + final Map hostCameraHandlers = {}; + + // The stream to receive frames from the native code. + StreamSubscription? _platformImageStreamSubscription; + + // The stream for vending frames to platform interface clients. + StreamController? _frameStreamController; + + Stream _cameraEvents(int cameraId) => + cameraEventStreamController.stream.where((CameraEvent event) => event.cameraId == cameraId); + + @override + Future> availableCameras() async { + try { + return (await _hostApi.getAvailableCameras()).map(cameraDescriptionFromPlatform).toList(); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future createCamera( + CameraDescription cameraDescription, + ResolutionPreset? resolutionPreset, { + bool enableAudio = false, + }) => createCameraWithSettings( + cameraDescription, + MediaSettings(resolutionPreset: resolutionPreset, enableAudio: enableAudio), + ); + + @override + Future createCameraWithSettings( + CameraDescription cameraDescription, + MediaSettings? mediaSettings, + ) async { + try { + return await _hostApi.create( + cameraDescription.name, + PlatformMediaSettings( + resolutionPreset: _pigeonResolutionPreset(mediaSettings?.resolutionPreset), + framesPerSecond: mediaSettings?.fps, + videoBitrate: mediaSettings?.videoBitrate, + audioBitrate: mediaSettings?.audioBitrate, + enableAudio: mediaSettings?.enableAudio ?? true, + ), + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future initializeCamera( + int cameraId, { + ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, + }) async { + hostCameraHandlers.putIfAbsent( + cameraId, + () => HostCameraMessageHandler(cameraId, cameraEventStreamController), + ); + + final completer = Completer(); + + unawaited( + onCameraInitialized(cameraId).first.then((CameraInitializedEvent value) { + completer.complete(); + }), + ); + + try { + await _hostApi.initialize(cameraId, _pigeonImageFormat(imageFormatGroup)); + } on PlatformException catch (e, s) { + completer.completeError(CameraException(e.code, e.message), s); + } + + return completer.future; + } + + @override + Future dispose(int cameraId) async { + final HostCameraMessageHandler? handler = hostCameraHandlers.remove(cameraId); + handler?.dispose(); + + await _hostApi.dispose(cameraId); + } + + @override + Stream onCameraInitialized(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onCameraResolutionChanged(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onCameraClosing(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onCameraError(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onVideoRecordedEvent(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onDeviceOrientationChanged() { + return hostHandler.deviceEventStreamController.stream + .whereType(); + } + + @override + Future lockCaptureOrientation(int cameraId, DeviceOrientation orientation) async { + await _hostApi.lockCaptureOrientation(serializeDeviceOrientation(orientation)); + } + + @override + Future unlockCaptureOrientation(int cameraId) async { + await _hostApi.unlockCaptureOrientation(); + } + + @override + Future takePicture(int cameraId) async { + final String path = await _hostApi.takePicture(); + return XFile(path); + } + + @override + Future prepareForVideoRecording() async { + await _hostApi.prepareForVideoRecording(); + } + + @override + Future startVideoRecording(int cameraId, {Duration? maxVideoDuration}) async { + // Ignore maxVideoDuration, as it is unimplemented and deprecated. + return startVideoCapturing(VideoCaptureOptions(cameraId)); + } + + @override + Future startVideoCapturing(VideoCaptureOptions options) async { + // Max video duration is currently not supported. + await _hostApi.startVideoRecording(options.streamCallback != null); + + if (options.streamCallback != null) { + _frameStreamController = _createStreamController(); + _frameStreamController!.stream.listen(options.streamCallback); + _startStreamListener(); + } + } + + @override + Future stopVideoRecording(int cameraId) async { + final String path = await _hostApi.stopVideoRecording(); + return XFile(path); + } + + @override + Future pauseVideoRecording(int cameraId) async { + await _hostApi.pauseVideoRecording(); + } + + @override + Future resumeVideoRecording(int cameraId) async { + await _hostApi.resumeVideoRecording(); + } + + @override + bool supportsImageStreaming() => true; + + @override + Stream onStreamedFrameAvailable( + int cameraId, { + CameraImageStreamOptions? options, + }) { + _frameStreamController = _createStreamController(onListen: _onFrameStreamListen); + return _frameStreamController!.stream; + } + + StreamController _createStreamController({void Function()? onListen}) { + return StreamController( + onListen: onListen ?? () {}, + onPause: _onFrameStreamPauseResume, + onResume: _onFrameStreamPauseResume, + onCancel: _onFrameStreamCancel, + ); + } + + void _onFrameStreamListen() { + _startPlatformStream(); + } + + Future _startPlatformStream() async { + await _hostApi.startImageStream(); + _startStreamListener(); + } + + void _startStreamListener() { + _platformImageStreamSubscription = imageDataStream().listen(( + PlatformCameraImageData imageData, + ) { + try { + _hostApi.receivedImageStreamData(); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + _frameStreamController!.add(cameraImageFromPlatformData(imageData)); + }); + } + + FutureOr _onFrameStreamCancel() async { + await _hostApi.stopImageStream(); + await _platformImageStreamSubscription?.cancel(); + _platformImageStreamSubscription = null; + _frameStreamController = null; + } + + void _onFrameStreamPauseResume() { + throw CameraException( + 'InvalidCall', + 'Pause and resume are not supported for onStreamedFrameAvailable', + ); + } + + @override + Future setFlashMode(int cameraId, FlashMode mode) async { + await _hostApi.setFlashMode(_pigeonFlashMode(mode)); + } + + @override + Future setExposureMode(int cameraId, ExposureMode mode) async { + await _hostApi.setExposureMode(_pigeonExposureMode(mode)); + } + + @override + Future setExposurePoint(int cameraId, Point? point) async { + assert(point == null || point.x >= 0 && point.x <= 1); + assert(point == null || point.y >= 0 && point.y <= 1); + + await _hostApi.setExposurePoint(_pigeonPoint(point)); + } + + @override + Future getMinExposureOffset(int cameraId) async { + return _hostApi.getMinExposureOffset(); + } + + @override + Future getMaxExposureOffset(int cameraId) async { + return _hostApi.getMaxExposureOffset(); + } + + @override + Future getExposureOffsetStepSize(int cameraId) async { + // iOS has no step size. + return 0; + } + + @override + Future setExposureOffset(int cameraId, double offset) async { + await _hostApi.setExposureOffset(offset); + // The platform API allows for implementations that have to adjust the + // target offset and return the actual offset used, but there is never + // adjustment in this implementation. + return offset; + } + + @override + Future setFocusMode(int cameraId, FocusMode mode) async { + await _hostApi.setFocusMode(_pigeonFocusMode(mode)); + } + + @override + Future setFocusPoint(int cameraId, Point? point) async { + assert(point == null || point.x >= 0 && point.x <= 1); + assert(point == null || point.y >= 0 && point.y <= 1); + + await _hostApi.setFocusPoint(_pigeonPoint(point)); + } + + @override + Future getMaxZoomLevel(int cameraId) async { + return _hostApi.getMaxZoomLevel(); + } + + @override + Future getMinZoomLevel(int cameraId) async { + return _hostApi.getMinZoomLevel(); + } + + @override + Future setZoomLevel(int cameraId, double zoom) async { + try { + await _hostApi.setZoomLevel(zoom); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future setVideoStabilizationMode(int cameraId, VideoStabilizationMode mode) async { + try { + final Map availableModes = + await _getSupportedVideoStabilizationModeMap(cameraId); + + final PlatformVideoStabilizationMode? platformMode = availableModes[mode]; + if (platformMode == null) { + throw ArgumentError('Unavailable video stabilization mode.', 'mode'); + } + await _hostApi.setVideoStabilizationMode(platformMode); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future> getSupportedVideoStabilizationModes(int cameraId) async { + return (await _getSupportedVideoStabilizationModeMap(cameraId)).keys; + } + + Future> + _getSupportedVideoStabilizationModeMap(int cameraId) async { + final ret = {}; + + for (final VideoStabilizationMode mode in VideoStabilizationMode.values) { + final PlatformVideoStabilizationMode? platformMode = _pigeonVideoStabilizationMode(mode); + if (platformMode != null) { + final bool isSupported = await _hostApi.isVideoStabilizationModeSupported(platformMode); + if (isSupported) { + ret[mode] = platformMode; + } + } + } + + return ret; + } + + @override + Future pausePreview(int cameraId) async { + await _hostApi.pausePreview(); + } + + @override + Future resumePreview(int cameraId) async { + await _hostApi.resumePreview(); + } + + @override + Future setDescriptionWhileRecording(CameraDescription description) async { + await _hostApi.updateDescriptionWhileRecording(description.name); + } + + @override + Future setImageFileFormat(int cameraId, ImageFileFormat format) async { + await _hostApi.setImageFileFormat(_pigeonImageFileFormat(format)); + } + + @override + Future setJpegImageQuality(int cameraId, int quality) async { + await _hostApi.setJpegImageQuality(quality); + } + + @override + Widget buildPreview(int cameraId) { + return Texture(textureId: cameraId); + } + + /// Returns an [FocusMode]'s Pigeon representation. + PlatformFocusMode _pigeonFocusMode(FocusMode mode) { + switch (mode) { + case FocusMode.locked: + return PlatformFocusMode.locked; + case FocusMode.auto: + return PlatformFocusMode.auto; + } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return PlatformFocusMode.auto; + } + + /// Returns an [ExposureMode]'s Pigeon representation. + PlatformExposureMode _pigeonExposureMode(ExposureMode mode) { + switch (mode) { + case ExposureMode.locked: + return PlatformExposureMode.locked; + case ExposureMode.auto: + return PlatformExposureMode.auto; + } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return PlatformExposureMode.auto; + } + + /// Returns a [FlashMode]'s Pigeon representation. + PlatformFlashMode _pigeonFlashMode(FlashMode flashMode) { + switch (flashMode) { + case FlashMode.off: + return PlatformFlashMode.off; + case FlashMode.auto: + return PlatformFlashMode.auto; + case FlashMode.always: + return PlatformFlashMode.always; + case FlashMode.torch: + return PlatformFlashMode.torch; + } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return PlatformFlashMode.off; + } + + /// Returns a [ResolutionPreset]'s Pigeon representation. + PlatformResolutionPreset _pigeonResolutionPreset(ResolutionPreset? resolutionPreset) { + if (resolutionPreset == null) { + // Provide a default if one isn't provided, since the native side needs + // to set something. + return PlatformResolutionPreset.high; + } + switch (resolutionPreset) { + case ResolutionPreset.max: + return PlatformResolutionPreset.max; + case ResolutionPreset.ultraHigh: + return PlatformResolutionPreset.ultraHigh; + case ResolutionPreset.veryHigh: + return PlatformResolutionPreset.veryHigh; + case ResolutionPreset.high: + return PlatformResolutionPreset.high; + case ResolutionPreset.medium: + return PlatformResolutionPreset.medium; + case ResolutionPreset.low: + return PlatformResolutionPreset.low; + } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return PlatformResolutionPreset.max; + } + + /// Returns a [VideoStabilizationMode]'s Pigeon representation. + PlatformVideoStabilizationMode? _pigeonVideoStabilizationMode( + VideoStabilizationMode videoStabilizationMode, + ) { + switch (videoStabilizationMode) { + case VideoStabilizationMode.off: + return PlatformVideoStabilizationMode.off; + case VideoStabilizationMode.level1: + return PlatformVideoStabilizationMode.standard; + case VideoStabilizationMode.level2: + return PlatformVideoStabilizationMode.cinematic; + case VideoStabilizationMode.level3: + return PlatformVideoStabilizationMode.cinematicExtended; + } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return null; + } + + /// Returns an [ImageFormatGroup]'s Pigeon representation. + PlatformImageFormatGroup _pigeonImageFormat(ImageFormatGroup format) { + switch (format) { + // "unknown" is used to indicate the default. + case ImageFormatGroup.unknown: + case ImageFormatGroup.bgra8888: + return PlatformImageFormatGroup.bgra8888; + case ImageFormatGroup.yuv420: + return PlatformImageFormatGroup.yuv420; + case ImageFormatGroup.jpeg: + case ImageFormatGroup.nv21: + // Fall through. + } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // TODO(stuartmorgan): Consider throwing an UnsupportedError, instead of + // doing fallback, when a specific unsupported format is requested. This + // would require a breaking change at this layer and the app-facing layer. + return PlatformImageFormatGroup.bgra8888; + } + + /// Returns an [ImageFileFormat]'s Pigeon representation. + PlatformImageFileFormat _pigeonImageFileFormat(ImageFileFormat format) { + switch (format) { + case ImageFileFormat.heif: + return PlatformImageFileFormat.heif; + case ImageFileFormat.jpeg: + return PlatformImageFileFormat.jpeg; + } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // TODO(stuartmorgan): Consider throwing an UnsupportedError, instead of + // doing fallback, when a specific unsupported format is requested. This + // would require a breaking change at this layer and the app-facing layer. + // ignore: dead_code + return PlatformImageFileFormat.jpeg; + } + + /// Returns a [Point]s Pigeon representation. + PlatformPoint? _pigeonPoint(Point? point) { + if (point == null) { + return null; + } + return PlatformPoint(x: point.x, y: point.y); + } +} + +/// Callback handler for device-level events from the platform host. +@visibleForTesting +class HostDeviceMessageHandler implements CameraGlobalEventApi { + /// Creates a new handler and registers it to listen to its platform channel. + HostDeviceMessageHandler() { + CameraGlobalEventApi.setUp(this); + } + + /// The controller used to broadcast general device events coming from the + /// host platform. + /// + /// It is a `broadcast` because multiple controllers will connect to + /// different stream views of this Controller. + final StreamController deviceEventStreamController = + StreamController.broadcast(); + + @override + void deviceOrientationChanged(PlatformDeviceOrientation orientation) { + deviceEventStreamController.add( + DeviceOrientationChangedEvent(deviceOrientationFromPlatform(orientation)), + ); + } +} + +/// Callback handler for camera-level events from the platform host. +@visibleForTesting +class HostCameraMessageHandler implements CameraEventApi { + /// Creates a new handler that listens for events from camera [cameraId], and + /// broadcasts them to [streamController]. + HostCameraMessageHandler(this.cameraId, this.streamController) { + CameraEventApi.setUp(this, messageChannelSuffix: cameraId.toString()); + } + + /// Removes the handler for native messages. + void dispose() { + CameraEventApi.setUp(null, messageChannelSuffix: cameraId.toString()); + } + + /// The camera ID this handler listens for events from. + final int cameraId; + + /// The controller used to broadcast camera events coming from the + /// host platform. + final StreamController streamController; + + @override + void error(String message) { + streamController.add(CameraErrorEvent(cameraId, message)); + } + + @override + void initialized(PlatformCameraState initialState) { + streamController.add( + CameraInitializedEvent( + cameraId, + initialState.previewSize.width, + initialState.previewSize.height, + exposureModeFromPlatform(initialState.exposureMode), + initialState.exposurePointSupported, + focusModeFromPlatform(initialState.focusMode), + initialState.focusPointSupported, + ), + ); + } +} diff --git a/camera_avfoundation/lib/src/messages.g.dart b/camera_avfoundation/lib/src/messages.g.dart new file mode 100644 index 0000000..525f21b --- /dev/null +++ b/camera_avfoundation/lib/src/messages.g.dart @@ -0,0 +1,1400 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v27.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: unused_import, unused_shown_name +// ignore_for_file: type=lint + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List; + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; + +Object? _extractReplyValueOrThrow( + List? replyList, + String channelName, { + required bool isNullValid, +}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; +} + +List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} + +bool _deepEquals(Object? a, Object? b) { + if (identical(a, b)) { + return true; + } + if (a is double && b is double) { + if (a.isNaN && b.isNaN) { + return true; + } + return a == b; + } + if (a is List && b is List) { + return a.length == b.length && + a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + } + if (a is Map && b is Map) { + if (a.length != b.length) { + return false; + } + for (final MapEntry entryA in a.entries) { + bool found = false; + for (final MapEntry entryB in b.entries) { + if (_deepEquals(entryA.key, entryB.key)) { + if (_deepEquals(entryA.value, entryB.value)) { + found = true; + break; + } else { + return false; + } + } + } + if (!found) { + return false; + } + } + return true; + } + return a == b; +} + +int _deepHash(Object? value) { + if (value is List) { + return Object.hashAll(value.map(_deepHash)); + } + if (value is Map) { + int result = 0; + for (final MapEntry entry in value.entries) { + result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); + } + return result; + } + if (value is double && value.isNaN) { + // Normalize NaN to a consistent hash. + return 0x7FF8000000000000.hashCode; + } + if (value is double && value == 0.0) { + // Normalize -0.0 to 0.0 so they have the same hash code. + return 0.0.hashCode; + } + return value.hashCode; +} + +enum PlatformCameraLensDirection { + /// Front facing camera (a user looking at the screen is seen by the camera). + front, + + /// Back facing camera (a user looking at the screen is not seen by the camera). + back, + + /// External camera which may not be mounted to the device. + external, +} + +enum PlatformCameraLensType { + /// A built-in wide-angle camera device type. + wide, + + /// A built-in camera device type with a longer focal length than a wide-angle camera. + telephoto, + + /// A built-in camera device type with a shorter focal length than a wide-angle camera. + ultraWide, + + /// Unknown camera device type. + unknown, +} + +enum PlatformDeviceOrientation { portraitUp, landscapeLeft, portraitDown, landscapeRight } + +enum PlatformExposureMode { auto, locked } + +enum PlatformFlashMode { off, auto, always, torch } + +enum PlatformFocusMode { auto, locked } + +/// Pigeon version of ImageFileFormat. +enum PlatformImageFileFormat { jpeg, heif } + +enum PlatformImageFormatGroup { bgra8888, yuv420 } + +enum PlatformResolutionPreset { low, medium, high, veryHigh, ultraHigh, max } + +enum PlatformVideoStabilizationMode { off, standard, cinematic, cinematicExtended } + +class PlatformCameraDescription { + PlatformCameraDescription({ + required this.name, + required this.lensDirection, + required this.lensType, + }); + + /// The name of the camera device. + String name; + + /// The direction the camera is facing. + PlatformCameraLensDirection lensDirection; + + /// The type of the camera lens. + PlatformCameraLensType lensType; + + List _toList() { + return [name, lensDirection, lensType]; + } + + Object encode() { + return _toList(); + } + + static PlatformCameraDescription decode(Object result) { + result as List; + return PlatformCameraDescription( + name: result[0]! as String, + lensDirection: result[1]! as PlatformCameraLensDirection, + lensType: result[2]! as PlatformCameraLensType, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformCameraDescription || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(name, other.name) && + _deepEquals(lensDirection, other.lensDirection) && + _deepEquals(lensType, other.lensType); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'PlatformCameraDescription(name: $name, lensDirection: $lensDirection, lensType: $lensType)'; + } +} + +class PlatformCameraState { + PlatformCameraState({ + required this.previewSize, + required this.exposureMode, + required this.focusMode, + required this.exposurePointSupported, + required this.focusPointSupported, + }); + + /// The size of the preview, in pixels. + PlatformSize previewSize; + + /// The default exposure mode + PlatformExposureMode exposureMode; + + /// The default focus mode + PlatformFocusMode focusMode; + + /// Whether setting exposure points is supported. + bool exposurePointSupported; + + /// Whether setting focus points is supported. + bool focusPointSupported; + + List _toList() { + return [ + previewSize, + exposureMode, + focusMode, + exposurePointSupported, + focusPointSupported, + ]; + } + + Object encode() { + return _toList(); + } + + static PlatformCameraState decode(Object result) { + result as List; + return PlatformCameraState( + previewSize: result[0]! as PlatformSize, + exposureMode: result[1]! as PlatformExposureMode, + focusMode: result[2]! as PlatformFocusMode, + exposurePointSupported: result[3]! as bool, + focusPointSupported: result[4]! as bool, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformCameraState || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(previewSize, other.previewSize) && + _deepEquals(exposureMode, other.exposureMode) && + _deepEquals(focusMode, other.focusMode) && + _deepEquals(exposurePointSupported, other.exposurePointSupported) && + _deepEquals(focusPointSupported, other.focusPointSupported); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'PlatformCameraState(previewSize: $previewSize, exposureMode: $exposureMode, focusMode: $focusMode, exposurePointSupported: $exposurePointSupported, focusPointSupported: $focusPointSupported)'; + } +} + +class PlatformCameraImageData { + PlatformCameraImageData({ + required this.formatCode, + required this.width, + required this.height, + required this.planes, + required this.lensAperture, + required this.sensorExposureTimeNanoseconds, + required this.sensorSensitivity, + }); + + /// The FourCharCode of the image format. + int formatCode; + + int width; + + int height; + + List planes; + + double lensAperture; + + int sensorExposureTimeNanoseconds; + + double sensorSensitivity; + + List _toList() { + return [ + formatCode, + width, + height, + planes, + lensAperture, + sensorExposureTimeNanoseconds, + sensorSensitivity, + ]; + } + + Object encode() { + return _toList(); + } + + static PlatformCameraImageData decode(Object result) { + result as List; + return PlatformCameraImageData( + formatCode: result[0]! as int, + width: result[1]! as int, + height: result[2]! as int, + planes: (result[3]! as List).cast(), + lensAperture: result[4]! as double, + sensorExposureTimeNanoseconds: result[5]! as int, + sensorSensitivity: result[6]! as double, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformCameraImageData || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(formatCode, other.formatCode) && + _deepEquals(width, other.width) && + _deepEquals(height, other.height) && + _deepEquals(planes, other.planes) && + _deepEquals(lensAperture, other.lensAperture) && + _deepEquals(sensorExposureTimeNanoseconds, other.sensorExposureTimeNanoseconds) && + _deepEquals(sensorSensitivity, other.sensorSensitivity); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'PlatformCameraImageData(formatCode: $formatCode, width: $width, height: $height, planes: $planes, lensAperture: $lensAperture, sensorExposureTimeNanoseconds: $sensorExposureTimeNanoseconds, sensorSensitivity: $sensorSensitivity)'; + } +} + +class PlatformCameraImagePlane { + PlatformCameraImagePlane({ + required this.bytes, + required this.bytesPerRow, + required this.width, + required this.height, + }); + + Uint8List bytes; + + int bytesPerRow; + + int width; + + int height; + + List _toList() { + return [bytes, bytesPerRow, width, height]; + } + + Object encode() { + return _toList(); + } + + static PlatformCameraImagePlane decode(Object result) { + result as List; + return PlatformCameraImagePlane( + bytes: result[0]! as Uint8List, + bytesPerRow: result[1]! as int, + width: result[2]! as int, + height: result[3]! as int, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformCameraImagePlane || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(bytes, other.bytes) && + _deepEquals(bytesPerRow, other.bytesPerRow) && + _deepEquals(width, other.width) && + _deepEquals(height, other.height); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'PlatformCameraImagePlane(bytes: $bytes, bytesPerRow: $bytesPerRow, width: $width, height: $height)'; + } +} + +class PlatformMediaSettings { + PlatformMediaSettings({ + required this.resolutionPreset, + this.framesPerSecond, + this.videoBitrate, + this.audioBitrate, + required this.enableAudio, + }); + + PlatformResolutionPreset resolutionPreset; + + int? framesPerSecond; + + int? videoBitrate; + + int? audioBitrate; + + bool enableAudio; + + List _toList() { + return [resolutionPreset, framesPerSecond, videoBitrate, audioBitrate, enableAudio]; + } + + Object encode() { + return _toList(); + } + + static PlatformMediaSettings decode(Object result) { + result as List; + return PlatformMediaSettings( + resolutionPreset: result[0]! as PlatformResolutionPreset, + framesPerSecond: result[1] as int?, + videoBitrate: result[2] as int?, + audioBitrate: result[3] as int?, + enableAudio: result[4]! as bool, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformMediaSettings || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(resolutionPreset, other.resolutionPreset) && + _deepEquals(framesPerSecond, other.framesPerSecond) && + _deepEquals(videoBitrate, other.videoBitrate) && + _deepEquals(audioBitrate, other.audioBitrate) && + _deepEquals(enableAudio, other.enableAudio); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'PlatformMediaSettings(resolutionPreset: $resolutionPreset, framesPerSecond: $framesPerSecond, videoBitrate: $videoBitrate, audioBitrate: $audioBitrate, enableAudio: $enableAudio)'; + } +} + +class PlatformPoint { + PlatformPoint({required this.x, required this.y}); + + double x; + + double y; + + List _toList() { + return [x, y]; + } + + Object encode() { + return _toList(); + } + + static PlatformPoint decode(Object result) { + result as List; + return PlatformPoint(x: result[0]! as double, y: result[1]! as double); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformPoint || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(x, other.x) && _deepEquals(y, other.y); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'PlatformPoint(x: $x, y: $y)'; + } +} + +class PlatformSize { + PlatformSize({required this.width, required this.height}); + + double width; + + double height; + + List _toList() { + return [width, height]; + } + + Object encode() { + return _toList(); + } + + static PlatformSize decode(Object result) { + result as List; + return PlatformSize(width: result[0]! as double, height: result[1]! as double); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformSize || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(width, other.width) && _deepEquals(height, other.height); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'PlatformSize(width: $width, height: $height)'; + } +} + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is PlatformCameraLensDirection) { + buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is PlatformCameraLensType) { + buffer.putUint8(130); + writeValue(buffer, value.index); + } else if (value is PlatformDeviceOrientation) { + buffer.putUint8(131); + writeValue(buffer, value.index); + } else if (value is PlatformExposureMode) { + buffer.putUint8(132); + writeValue(buffer, value.index); + } else if (value is PlatformFlashMode) { + buffer.putUint8(133); + writeValue(buffer, value.index); + } else if (value is PlatformFocusMode) { + buffer.putUint8(134); + writeValue(buffer, value.index); + } else if (value is PlatformImageFileFormat) { + buffer.putUint8(135); + writeValue(buffer, value.index); + } else if (value is PlatformImageFormatGroup) { + buffer.putUint8(136); + writeValue(buffer, value.index); + } else if (value is PlatformResolutionPreset) { + buffer.putUint8(137); + writeValue(buffer, value.index); + } else if (value is PlatformVideoStabilizationMode) { + buffer.putUint8(138); + writeValue(buffer, value.index); + } else if (value is PlatformCameraDescription) { + buffer.putUint8(139); + writeValue(buffer, value.encode()); + } else if (value is PlatformCameraState) { + buffer.putUint8(140); + writeValue(buffer, value.encode()); + } else if (value is PlatformCameraImageData) { + buffer.putUint8(141); + writeValue(buffer, value.encode()); + } else if (value is PlatformCameraImagePlane) { + buffer.putUint8(142); + writeValue(buffer, value.encode()); + } else if (value is PlatformMediaSettings) { + buffer.putUint8(143); + writeValue(buffer, value.encode()); + } else if (value is PlatformPoint) { + buffer.putUint8(144); + writeValue(buffer, value.encode()); + } else if (value is PlatformSize) { + buffer.putUint8(145); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + final value = readValue(buffer) as int?; + return value == null ? null : PlatformCameraLensDirection.values[value]; + case 130: + final value = readValue(buffer) as int?; + return value == null ? null : PlatformCameraLensType.values[value]; + case 131: + final value = readValue(buffer) as int?; + return value == null ? null : PlatformDeviceOrientation.values[value]; + case 132: + final value = readValue(buffer) as int?; + return value == null ? null : PlatformExposureMode.values[value]; + case 133: + final value = readValue(buffer) as int?; + return value == null ? null : PlatformFlashMode.values[value]; + case 134: + final value = readValue(buffer) as int?; + return value == null ? null : PlatformFocusMode.values[value]; + case 135: + final value = readValue(buffer) as int?; + return value == null ? null : PlatformImageFileFormat.values[value]; + case 136: + final value = readValue(buffer) as int?; + return value == null ? null : PlatformImageFormatGroup.values[value]; + case 137: + final value = readValue(buffer) as int?; + return value == null ? null : PlatformResolutionPreset.values[value]; + case 138: + final value = readValue(buffer) as int?; + return value == null ? null : PlatformVideoStabilizationMode.values[value]; + case 139: + return PlatformCameraDescription.decode(readValue(buffer)!); + case 140: + return PlatformCameraState.decode(readValue(buffer)!); + case 141: + return PlatformCameraImageData.decode(readValue(buffer)!); + case 142: + return PlatformCameraImagePlane.decode(readValue(buffer)!); + case 143: + return PlatformMediaSettings.decode(readValue(buffer)!); + case 144: + return PlatformPoint.decode(readValue(buffer)!); + case 145: + return PlatformSize.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec(_PigeonCodec()); + +class CameraApi { + /// Constructor for [CameraApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + CameraApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + /// Returns the list of available cameras. + Future> getAvailableCameras() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.getAvailableCameras$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return (pigeonVar_replyValue! as List).cast(); + } + + /// Create a new camera with the given settings, and returns its ID. + Future create(String cameraName, PlatformMediaSettings settings) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.create$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([ + cameraName, + settings, + ]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as int; + } + + /// Initializes the camera with the given ID. + Future initialize(int cameraId, PlatformImageFormatGroup imageFormat) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.initialize$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([ + cameraId, + imageFormat, + ]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Begins streaming frames from the camera. + Future startImageStream() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.startImageStream$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Stops streaming frames from the camera. + Future stopImageStream() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.stopImageStream$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Called by the Dart side of the plugin when it has received the last image + /// frame sent. + /// + /// This is used to throttle sending frames across the channel. + Future receivedImageStreamData() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.receivedImageStreamData$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Indicates that the given camera is no longer being used on the Dart side, + /// and any associated resources can be cleaned up. + Future dispose(int cameraId) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.dispose$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([cameraId]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Locks the camera capture to the current device orientation. + Future lockCaptureOrientation(PlatformDeviceOrientation orientation) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.lockCaptureOrientation$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([orientation]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Unlocks camera capture orientation, allowing it to automatically adapt to + /// device orientation. + Future unlockCaptureOrientation() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.unlockCaptureOrientation$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Takes a picture with the current settings, and returns the path to the + /// resulting file. + Future takePicture() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.takePicture$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as String; + } + + /// Does any preprocessing necessary before beginning to record video. + Future prepareForVideoRecording() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.prepareForVideoRecording$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Begins recording video, optionally enabling streaming to Dart at the same + /// time. + Future startVideoRecording(bool enableStream) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.startVideoRecording$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([enableStream]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Stops recording video, and results the path to the resulting file. + Future stopVideoRecording() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.stopVideoRecording$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as String; + } + + /// Pauses video recording. + Future pauseVideoRecording() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.pauseVideoRecording$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Resumes a previously paused video recording. + Future resumeVideoRecording() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.resumeVideoRecording$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Switches the camera to the given flash mode. + Future setFlashMode(PlatformFlashMode mode) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.setFlashMode$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([mode]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Switches the camera to the given exposure mode. + Future setExposureMode(PlatformExposureMode mode) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.setExposureMode$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([mode]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Anchors auto-exposure to the given point in (0,1) coordinate space. + /// + /// A null value resets to the default exposure point. + Future setExposurePoint(PlatformPoint? point) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.setExposurePoint$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([point]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Returns the minimum exposure offset supported by the camera. + Future getMinExposureOffset() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.getMinExposureOffset$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as double; + } + + /// Returns the maximum exposure offset supported by the camera. + Future getMaxExposureOffset() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.getMaxExposureOffset$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as double; + } + + /// Sets the exposure offset manually to the given value. + Future setExposureOffset(double offset) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.setExposureOffset$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([offset]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Switches the camera to the given focus mode. + Future setFocusMode(PlatformFocusMode mode) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.setFocusMode$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([mode]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Anchors auto-focus to the given point in (0,1) coordinate space. + /// + /// A null value resets to the default focus point. + Future setFocusPoint(PlatformPoint? point) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.setFocusPoint$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([point]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Returns the minimum zoom level supported by the camera. + Future getMinZoomLevel() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.getMinZoomLevel$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as double; + } + + /// Returns the maximum zoom level supported by the camera. + Future getMaxZoomLevel() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.getMaxZoomLevel$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as double; + } + + /// Sets the zoom factor. + Future setZoomLevel(double zoom) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.setZoomLevel$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([zoom]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Sets the video stabilization mode. + Future setVideoStabilizationMode(PlatformVideoStabilizationMode mode) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.setVideoStabilizationMode$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([mode]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Gets if the given video stabilization mode is supported. + Future isVideoStabilizationModeSupported(PlatformVideoStabilizationMode mode) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.isVideoStabilizationModeSupported$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([mode]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as bool; + } + + /// Pauses streaming of preview frames. + Future pausePreview() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.pausePreview$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Resumes a previously paused preview stream. + Future resumePreview() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.resumePreview$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Changes the camera used while recording video. + /// + /// This should only be called while video recording is active. + Future updateDescriptionWhileRecording(String cameraName) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.updateDescriptionWhileRecording$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([cameraName]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Sets the file format used for taking pictures. + Future setImageFileFormat(PlatformImageFileFormat format) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.setImageFileFormat$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([format]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + /// Sets the JPEG compression quality for still image capture. + Future setJpegImageQuality(int quality) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.setJpegImageQuality$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([quality]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } +} + +/// Returns a broadcast [Stream] of events from the `imageDataStream` event channel. +/// +/// Each call to this method creates a new [EventChannel], so it should +/// not be called multiple times for the same `instanceName`. To deliver +/// events to multiple listeners, call this method once and listen to the +/// returned broadcast stream multiple times instead. +Stream imageDataStream({String instanceName = ''}) { + if (instanceName.isNotEmpty) { + instanceName = '.$instanceName'; + } + final EventChannel imageDataStreamChannel = EventChannel( + 'dev.flutter.pigeon.camera_avfoundation.CameraImageStreamEventApi.imageDataStream$instanceName', + pigeonMethodCodec, + ); + return imageDataStreamChannel.receiveBroadcastStream().map((dynamic event) { + return event as PlatformCameraImageData; + }); +} + +/// Handler for native callbacks that are not tied to a specific camera ID. +abstract class CameraGlobalEventApi { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + /// Called when the device's physical orientation changes. + void deviceOrientationChanged(PlatformDeviceOrientation orientation); + + static void setUp( + CameraGlobalEventApi? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.camera_avfoundation.CameraGlobalEventApi.deviceOrientationChanged$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + final List args = message! as List; + final PlatformDeviceOrientation arg_orientation = args[0]! as PlatformDeviceOrientation; + try { + api.deviceOrientationChanged(arg_orientation); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); + } + }); + } + } + } +} + +/// Handler for native callbacks that are tied to a specific camera ID. +/// +/// This is intended to be initialized with the camera ID as a suffix. +abstract class CameraEventApi { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + /// Called when the camera is inialitized for use. + void initialized(PlatformCameraState initialState); + + /// Called when an error occurs in the camera. + /// + /// This should be used for errors that occur outside of the context of + /// handling a specific HostApi call, such as during streaming. + void error(String message); + + static void setUp( + CameraEventApi? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.camera_avfoundation.CameraEventApi.initialized$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + final List args = message! as List; + final PlatformCameraState arg_initialState = args[0]! as PlatformCameraState; + try { + api.initialized(arg_initialState); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); + } + }); + } + } + { + final pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.camera_avfoundation.CameraEventApi.error$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + final List args = message! as List; + final String arg_message = args[0]! as String; + try { + api.error(arg_message); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); + } + }); + } + } + } +} diff --git a/camera_avfoundation/lib/src/type_conversion.dart b/camera_avfoundation/lib/src/type_conversion.dart new file mode 100644 index 0000000..213b355 --- /dev/null +++ b/camera_avfoundation/lib/src/type_conversion.dart @@ -0,0 +1,50 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_platform_interface/camera_platform_interface.dart'; + +import 'messages.g.dart'; + +/// Converts method channel call [data] for `receivedImageStreamData` to a +/// [CameraImageData]. +CameraImageData cameraImageFromPlatformData(PlatformCameraImageData data) { + return CameraImageData( + format: _cameraImageFormatFromPlatformImageFormat(data.formatCode), + width: data.width, + height: data.height, + lensAperture: data.lensAperture, + sensorExposureTime: data.sensorExposureTimeNanoseconds, + sensorSensitivity: data.sensorSensitivity, + planes: List.unmodifiable( + data.planes.map( + (PlatformCameraImagePlane planeData) => _cameraImagePlaneFromPlatformData(planeData), + ), + ), + ); +} + +CameraImageFormat _cameraImageFormatFromPlatformImageFormat(int data) { + return CameraImageFormat(_imageFormatGroupFromPlatformImageFormat(data), raw: data); +} + +ImageFormatGroup _imageFormatGroupFromPlatformImageFormat(int data) { + switch (data) { + case 875704438: // kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange + return ImageFormatGroup.yuv420; + + case 1111970369: // kCVPixelFormatType_32BGRA + return ImageFormatGroup.bgra8888; + } + + return ImageFormatGroup.unknown; +} + +CameraImagePlane _cameraImagePlaneFromPlatformData(PlatformCameraImagePlane data) { + return CameraImagePlane( + bytes: data.bytes, + bytesPerRow: data.bytesPerRow, + width: data.width, + height: data.height, + ); +} diff --git a/camera_avfoundation/lib/src/utils.dart b/camera_avfoundation/lib/src/utils.dart new file mode 100644 index 0000000..4e883cd --- /dev/null +++ b/camera_avfoundation/lib/src/utils.dart @@ -0,0 +1,84 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart'; + +import 'messages.g.dart'; + +/// Creates a [CameraDescription] from a Pigeon [PlatformCameraDescription]. +CameraDescription cameraDescriptionFromPlatform(PlatformCameraDescription camera) { + return CameraDescription( + name: camera.name, + lensDirection: cameraLensDirectionFromPlatform(camera.lensDirection), + sensorOrientation: 90, + lensType: cameraLensTypeFromPlatform(camera.lensType), + ); +} + +/// Converts a Pigeon [PlatformCameraLensDirection] to a [CameraLensDirection]. +CameraLensDirection cameraLensDirectionFromPlatform(PlatformCameraLensDirection direction) { + return switch (direction) { + PlatformCameraLensDirection.front => CameraLensDirection.front, + PlatformCameraLensDirection.back => CameraLensDirection.back, + PlatformCameraLensDirection.external => CameraLensDirection.external, + }; +} + +/// Converts a Pigeon [PlatformCameraLensType] to a [CameraLensType]. +CameraLensType cameraLensTypeFromPlatform(PlatformCameraLensType type) { + return switch (type) { + PlatformCameraLensType.wide => CameraLensType.wide, + PlatformCameraLensType.telephoto => CameraLensType.telephoto, + PlatformCameraLensType.ultraWide => CameraLensType.ultraWide, + PlatformCameraLensType.unknown => CameraLensType.unknown, + }; +} + +/// Convents the given device orientation to Pigeon. +PlatformDeviceOrientation serializeDeviceOrientation(DeviceOrientation orientation) { + switch (orientation) { + case DeviceOrientation.portraitUp: + return PlatformDeviceOrientation.portraitUp; + case DeviceOrientation.portraitDown: + return PlatformDeviceOrientation.portraitDown; + case DeviceOrientation.landscapeRight: + return PlatformDeviceOrientation.landscapeRight; + case DeviceOrientation.landscapeLeft: + return PlatformDeviceOrientation.landscapeLeft; + } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return PlatformDeviceOrientation.portraitUp; +} + +/// Converts a Pigeon [PlatformDeviceOrientation] to a [DeviceOrientation]. +DeviceOrientation deviceOrientationFromPlatform(PlatformDeviceOrientation orientation) { + return switch (orientation) { + PlatformDeviceOrientation.portraitUp => DeviceOrientation.portraitUp, + PlatformDeviceOrientation.portraitDown => DeviceOrientation.portraitDown, + PlatformDeviceOrientation.landscapeLeft => DeviceOrientation.landscapeLeft, + PlatformDeviceOrientation.landscapeRight => DeviceOrientation.landscapeRight, + }; +} + +/// Converts a Pigeon [PlatformExposureMode] to an [ExposureMode]. +ExposureMode exposureModeFromPlatform(PlatformExposureMode mode) { + return switch (mode) { + PlatformExposureMode.auto => ExposureMode.auto, + PlatformExposureMode.locked => ExposureMode.locked, + }; +} + +/// Converts a Pigeon [PlatformFocusMode] to an [FocusMode]. +FocusMode focusModeFromPlatform(PlatformFocusMode mode) { + return switch (mode) { + PlatformFocusMode.auto => FocusMode.auto, + PlatformFocusMode.locked => FocusMode.locked, + }; +} diff --git a/camera_avfoundation/pigeons/copyright.txt b/camera_avfoundation/pigeons/copyright.txt new file mode 100644 index 0000000..07e5f85 --- /dev/null +++ b/camera_avfoundation/pigeons/copyright.txt @@ -0,0 +1,3 @@ +Copyright 2013 The Flutter Authors +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. diff --git a/camera_avfoundation/pigeons/messages.dart b/camera_avfoundation/pigeons/messages.dart new file mode 100644 index 0000000..c726c65 --- /dev/null +++ b/camera_avfoundation/pigeons/messages.dart @@ -0,0 +1,375 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + swiftOut: 'darwin/camera_avfoundation/Sources/camera_avfoundation/Messages.swift', + copyrightHeader: 'pigeons/copyright.txt', + ), +) +// Pigeon version of CameraLensDirection. +enum PlatformCameraLensDirection { + /// Front facing camera (a user looking at the screen is seen by the camera). + front, + + /// Back facing camera (a user looking at the screen is not seen by the camera). + back, + + /// External camera which may not be mounted to the device. + external, +} + +// Pigeon version of CameraLensDirection. +enum PlatformCameraLensType { + /// A built-in wide-angle camera device type. + wide, + + /// A built-in camera device type with a longer focal length than a wide-angle camera. + telephoto, + + /// A built-in camera device type with a shorter focal length than a wide-angle camera. + ultraWide, + + /// Unknown camera device type. + unknown, +} + +// Pigeon version of DeviceOrientation. +enum PlatformDeviceOrientation { portraitUp, landscapeLeft, portraitDown, landscapeRight } + +// Pigeon version of ExposureMode. +enum PlatformExposureMode { auto, locked } + +// Pigeon version of FlashMode. +enum PlatformFlashMode { off, auto, always, torch } + +// Pigeon version of FocusMode. +enum PlatformFocusMode { auto, locked } + +/// Pigeon version of ImageFileFormat. +enum PlatformImageFileFormat { jpeg, heif } + +// Pigeon version of the subset of ImageFormatGroup supported on iOS. +enum PlatformImageFormatGroup { bgra8888, yuv420 } + +// Pigeon version of ResolutionPreset. +enum PlatformResolutionPreset { low, medium, high, veryHigh, ultraHigh, max } + +enum PlatformVideoStabilizationMode { off, standard, cinematic, cinematicExtended } + +// Pigeon version of CameraDescription. +class PlatformCameraDescription { + PlatformCameraDescription({ + required this.name, + required this.lensDirection, + required this.lensType, + }); + + /// The name of the camera device. + final String name; + + /// The direction the camera is facing. + final PlatformCameraLensDirection lensDirection; + + /// The type of the camera lens. + final PlatformCameraLensType lensType; +} + +// Pigeon version of the data needed for a CameraInitializedEvent. +class PlatformCameraState { + PlatformCameraState({ + required this.previewSize, + required this.exposureMode, + required this.focusMode, + required this.exposurePointSupported, + required this.focusPointSupported, + }); + + /// The size of the preview, in pixels. + final PlatformSize previewSize; + + /// The default exposure mode + final PlatformExposureMode exposureMode; + + /// The default focus mode + final PlatformFocusMode focusMode; + + /// Whether setting exposure points is supported. + final bool exposurePointSupported; + + /// Whether setting focus points is supported. + final bool focusPointSupported; +} + +// Pigeon version of the data needed for a CameraImageData. +class PlatformCameraImageData { + PlatformCameraImageData({ + required this.formatCode, + required this.width, + required this.height, + required this.planes, + required this.lensAperture, + required this.sensorExposureTimeNanoseconds, + required this.sensorSensitivity, + }); + + /// The FourCharCode of the image format. + final int formatCode; + + final int width; + final int height; + final List planes; + final double lensAperture; + final int sensorExposureTimeNanoseconds; + final double sensorSensitivity; +} + +// Pigeon version of the data needed for a CameraImagePlane. +class PlatformCameraImagePlane { + const PlatformCameraImagePlane({ + required this.bytes, + required this.bytesPerRow, + required this.width, + required this.height, + }); + + final Uint8List bytes; + final int bytesPerRow; + final int width; + final int height; +} + +// Pigeon version of to MediaSettings. +class PlatformMediaSettings { + PlatformMediaSettings({ + required this.resolutionPreset, + required this.framesPerSecond, + required this.videoBitrate, + required this.audioBitrate, + required this.enableAudio, + }); + + final PlatformResolutionPreset resolutionPreset; + final int? framesPerSecond; + final int? videoBitrate; + final int? audioBitrate; + final bool enableAudio; +} + +// Pigeon equivalent of CGPoint. +class PlatformPoint { + PlatformPoint({required this.x, required this.y}); + + final double x; + final double y; +} + +// Pigeon equivalent of CGSize. +class PlatformSize { + PlatformSize({required this.width, required this.height}); + + final double width; + final double height; +} + +@HostApi() +abstract class CameraApi { + /// Returns the list of available cameras. + @async + @ObjCSelector('availableCamerasWithCompletion') + List getAvailableCameras(); + + /// Create a new camera with the given settings, and returns its ID. + @async + @ObjCSelector('createCameraWithName:settings:') + int create(String cameraName, PlatformMediaSettings settings); + + /// Initializes the camera with the given ID. + @async + @ObjCSelector('initializeCamera:withImageFormat:') + void initialize(int cameraId, PlatformImageFormatGroup imageFormat); + + /// Begins streaming frames from the camera. + @async + void startImageStream(); + + /// Stops streaming frames from the camera. + @async + void stopImageStream(); + + /// Called by the Dart side of the plugin when it has received the last image + /// frame sent. + /// + /// This is used to throttle sending frames across the channel. + @async + void receivedImageStreamData(); + + /// Indicates that the given camera is no longer being used on the Dart side, + /// and any associated resources can be cleaned up. + @async + @ObjCSelector('disposeCamera:') + void dispose(int cameraId); + + /// Locks the camera capture to the current device orientation. + @async + @ObjCSelector('lockCaptureOrientation:') + void lockCaptureOrientation(PlatformDeviceOrientation orientation); + + /// Unlocks camera capture orientation, allowing it to automatically adapt to + /// device orientation. + @async + void unlockCaptureOrientation(); + + /// Takes a picture with the current settings, and returns the path to the + /// resulting file. + @async + String takePicture(); + + /// Does any preprocessing necessary before beginning to record video. + @async + void prepareForVideoRecording(); + + /// Begins recording video, optionally enabling streaming to Dart at the same + /// time. + @async + @ObjCSelector('startVideoRecordingWithStreaming:') + void startVideoRecording(bool enableStream); + + /// Stops recording video, and results the path to the resulting file. + @async + String stopVideoRecording(); + + /// Pauses video recording. + @async + void pauseVideoRecording(); + + /// Resumes a previously paused video recording. + @async + void resumeVideoRecording(); + + /// Switches the camera to the given flash mode. + @async + @ObjCSelector('setFlashMode:') + void setFlashMode(PlatformFlashMode mode); + + /// Switches the camera to the given exposure mode. + @async + @ObjCSelector('setExposureMode:') + void setExposureMode(PlatformExposureMode mode); + + /// Anchors auto-exposure to the given point in (0,1) coordinate space. + /// + /// A null value resets to the default exposure point. + @async + @ObjCSelector('setExposurePoint:') + void setExposurePoint(PlatformPoint? point); + + /// Returns the minimum exposure offset supported by the camera. + @async + @ObjCSelector('getMinimumExposureOffset') + double getMinExposureOffset(); + + /// Returns the maximum exposure offset supported by the camera. + @async + @ObjCSelector('getMaximumExposureOffset') + double getMaxExposureOffset(); + + /// Sets the exposure offset manually to the given value. + @async + @ObjCSelector('setExposureOffset:') + void setExposureOffset(double offset); + + /// Switches the camera to the given focus mode. + @async + @ObjCSelector('setFocusMode:') + void setFocusMode(PlatformFocusMode mode); + + /// Anchors auto-focus to the given point in (0,1) coordinate space. + /// + /// A null value resets to the default focus point. + @async + @ObjCSelector('setFocusPoint:') + void setFocusPoint(PlatformPoint? point); + + /// Returns the minimum zoom level supported by the camera. + @async + @ObjCSelector('getMinimumZoomLevel') + double getMinZoomLevel(); + + /// Returns the maximum zoom level supported by the camera. + @async + @ObjCSelector('getMaximumZoomLevel') + double getMaxZoomLevel(); + + /// Sets the zoom factor. + @async + @ObjCSelector('setZoomLevel:') + void setZoomLevel(double zoom); + + /// Sets the video stabilization mode. + @async + @ObjCSelector('setVideoStabilizationMode:') + void setVideoStabilizationMode(PlatformVideoStabilizationMode mode); + + /// Gets if the given video stabilization mode is supported. + @async + @ObjCSelector('isVideoStabilizationModeSupported:') + bool isVideoStabilizationModeSupported(PlatformVideoStabilizationMode mode); + + /// Pauses streaming of preview frames. + @async + void pausePreview(); + + /// Resumes a previously paused preview stream. + @async + void resumePreview(); + + /// Changes the camera used while recording video. + /// + /// This should only be called while video recording is active. + @async + void updateDescriptionWhileRecording(String cameraName); + + /// Sets the file format used for taking pictures. + @async + @ObjCSelector('setImageFileFormat:') + void setImageFileFormat(PlatformImageFileFormat format); + + /// Sets the JPEG compression quality for still image capture. + @async + @ObjCSelector('setJpegImageQuality:') + void setJpegImageQuality(int quality); +} + +@EventChannelApi() +abstract class CameraImageStreamEventApi { + PlatformCameraImageData imageDataStream(); +} + +/// Handler for native callbacks that are not tied to a specific camera ID. +@FlutterApi() +abstract class CameraGlobalEventApi { + /// Called when the device's physical orientation changes. + void deviceOrientationChanged(PlatformDeviceOrientation orientation); +} + +/// Handler for native callbacks that are tied to a specific camera ID. +/// +/// This is intended to be initialized with the camera ID as a suffix. +@FlutterApi() +abstract class CameraEventApi { + /// Called when the camera is inialitized for use. + @ObjCSelector('initializedWithState:') + void initialized(PlatformCameraState initialState); + + /// Called when an error occurs in the camera. + /// + /// This should be used for errors that occur outside of the context of + /// handling a specific HostApi call, such as during streaming. + @ObjCSelector('reportError:') + void error(String message); +} diff --git a/camera_avfoundation/pubspec.yaml b/camera_avfoundation/pubspec.yaml new file mode 100644 index 0000000..5da621f --- /dev/null +++ b/camera_avfoundation/pubspec.yaml @@ -0,0 +1,37 @@ +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 +version: 0.10.3 + +environment: + sdk: ^3.10.0 + flutter: ">=3.38.0" + +flutter: + plugin: + implements: camera + platforms: + ios: + pluginClass: CameraPlugin + dartPluginClass: AVFoundationCamera + sharedDarwinSource: true + +dependencies: + camera_platform_interface: ^2.13.0 + flutter: + sdk: flutter + meta: ^1.10.0 + stream_transform: ^2.0.0 + +dev_dependencies: + async: ^2.5.0 + build_runner: ^2.4.9 + flutter_test: + sdk: flutter + leak_tracker_flutter_testing: any + mockito: ^5.4.4 + pigeon: ^27.3.2 + +topics: + - camera diff --git a/camera_avfoundation/test/avfoundation_camera_test.dart b/camera_avfoundation/test/avfoundation_camera_test.dart new file mode 100644 index 0000000..a1f6c16 --- /dev/null +++ b/camera_avfoundation/test/avfoundation_camera_test.dart @@ -0,0 +1,798 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:math'; + +import 'package:async/async.dart'; +import 'package:camera_avfoundation/src/avfoundation_camera.dart'; +import 'package:camera_avfoundation/src/messages.g.dart'; +import 'package:camera_avfoundation/src/utils.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'avfoundation_camera_test.mocks.dart'; + +@GenerateNiceMocks(>[MockSpec()]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('registers instance', () async { + AVFoundationCamera.registerWith(); + expect(CameraPlatform.instance, isA()); + }); + + group('Creation, Initialization & Disposal Tests', () { + test('Should send creation data and receive back a camera id', () async { + // Arrange + final mockApi = MockCameraApi(); + when(mockApi.create(any, any)).thenAnswer((_) async => 1); + final camera = AVFoundationCamera(api: mockApi); + const cameraName = 'Test'; + + // Act + final int cameraId = await camera.createCamera( + const CameraDescription( + name: cameraName, + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + + // Assert + final VerificationResult verification = verify(mockApi.create(captureAny, captureAny)); + expect(verification.captured[0], cameraName); + final settings = verification.captured[1] as PlatformMediaSettings?; + expect(settings, isNotNull); + expect(settings?.resolutionPreset, PlatformResolutionPreset.high); + expect(cameraId, 1); + }); + + test( + 'Should send creation data and receive back a camera id using createCameraWithSettings', + () async { + // Arrange + final mockApi = MockCameraApi(); + when(mockApi.create(any, any)).thenAnswer((_) async => 1); + final camera = AVFoundationCamera(api: mockApi); + const cameraName = 'Test'; + const fps = 15; + const videoBitrate = 200000; + const audioBitrate = 32000; + + // Act + final int cameraId = await camera.createCameraWithSettings( + const CameraDescription( + name: cameraName, + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + const MediaSettings( + resolutionPreset: ResolutionPreset.low, + fps: fps, + videoBitrate: videoBitrate, + audioBitrate: audioBitrate, + enableAudio: true, + ), + ); + + // Assert + final VerificationResult verification = verify(mockApi.create(captureAny, captureAny)); + expect(verification.captured[0], cameraName); + final settings = verification.captured[1] as PlatformMediaSettings?; + expect(settings, isNotNull); + expect(settings?.resolutionPreset, PlatformResolutionPreset.low); + expect(settings?.framesPerSecond, fps); + expect(settings?.videoBitrate, videoBitrate); + expect(settings?.audioBitrate, audioBitrate); + expect(settings?.enableAudio, true); + expect(cameraId, 1); + }, + ); + + test('Should throw CameraException when create throws a PlatformException', () { + // Arrange + const exceptionCode = 'TESTING_ERROR_CODE'; + const exceptionMessage = 'Mock error message used during testing.'; + final mockApi = MockCameraApi(); + when(mockApi.create(any, any)).thenAnswer((_) async { + throw PlatformException(code: exceptionCode, message: exceptionMessage); + }); + final camera = AVFoundationCamera(api: mockApi); + + // Act + expect( + () => camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ), + throwsA( + isA() + .having((CameraException e) => e.code, 'code', exceptionCode) + .having((CameraException e) => e.description, 'description', exceptionMessage), + ), + ); + }); + + test('Should throw CameraException when initialize throws a PlatformException', () { + // Arrange + const exceptionCode = 'TESTING_ERROR_CODE'; + const exceptionMessage = 'Mock error message used during testing.'; + final mockApi = MockCameraApi(); + when(mockApi.initialize(any, any)).thenAnswer((_) async { + throw PlatformException(code: exceptionCode, message: exceptionMessage); + }); + final camera = AVFoundationCamera(api: mockApi); + + // Act + expect( + () => camera.initializeCamera(0), + throwsA( + isA() + .having((CameraException e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having( + (CameraException e) => e.description, + 'description', + 'Mock error message used during testing.', + ), + ), + ); + }); + + test('Should send initialization data', () async { + // Arrange + final mockApi = MockCameraApi(); + final camera = AVFoundationCamera(api: mockApi); + final int cameraId = await camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + + // Act + final Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add( + CameraInitializedEvent(cameraId, 1920, 1080, ExposureMode.auto, true, FocusMode.auto, true), + ); + await initializeFuture; + + // Assert + final VerificationResult verification = verify(mockApi.initialize(captureAny, captureAny)); + expect(verification.captured[0], cameraId); + // The default when unspecified should be bgra8888. + expect(verification.captured[1], PlatformImageFormatGroup.bgra8888); + }); + + test('Should send a disposal call on dispose', () async { + // Arrange + final mockApi = MockCameraApi(); + final camera = AVFoundationCamera(api: mockApi); + final int cameraId = await camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + final Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add( + CameraInitializedEvent(cameraId, 1920, 1080, ExposureMode.auto, true, FocusMode.auto, true), + ); + await initializeFuture; + + // Act + await camera.dispose(cameraId); + + // Assert + final VerificationResult verification = verify(mockApi.dispose(captureAny)); + expect(verification.captured[0], cameraId); + }); + }); + + group('Event Tests', () { + late AVFoundationCamera camera; + late int cameraId; + setUp(() async { + final mockApi = MockCameraApi(); + when(mockApi.create(any, any)).thenAnswer((_) async => 1); + camera = AVFoundationCamera(api: mockApi); + cameraId = await camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + final Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add( + CameraInitializedEvent(cameraId, 1920, 1080, ExposureMode.auto, true, FocusMode.auto, true), + ); + await initializeFuture; + }); + + test('Should receive initialized event', () async { + // Act + final Stream eventStream = camera.onCameraInitialized(cameraId); + final streamQueue = StreamQueue(eventStream); + + final previewSize = PlatformSize(width: 3840, height: 2160); + // Emit test events + final event = CameraInitializedEvent( + cameraId, + previewSize.width, + previewSize.height, + ExposureMode.auto, + true, + FocusMode.auto, + true, + ); + camera.hostCameraHandlers[cameraId]!.initialized( + PlatformCameraState( + previewSize: previewSize, + exposureMode: PlatformExposureMode.auto, + focusMode: PlatformFocusMode.auto, + exposurePointSupported: true, + focusPointSupported: true, + ), + ); + + // Assert + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive camera error events', () async { + // Act + final Stream errorStream = camera.onCameraError(cameraId); + final streamQueue = StreamQueue(errorStream); + + // Emit test events + const errorMessage = 'Error Description'; + final event = CameraErrorEvent(cameraId, errorMessage); + camera.hostCameraHandlers[cameraId]!.error(errorMessage); + camera.hostCameraHandlers[cameraId]!.error(errorMessage); + camera.hostCameraHandlers[cameraId]!.error(errorMessage); + + // Assert + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive device orientation change events', () async { + // Act + final Stream eventStream = camera.onDeviceOrientationChanged(); + final streamQueue = StreamQueue(eventStream); + + // Emit test events + const event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + for (var i = 0; i < 3; i++) { + camera.hostHandler.deviceOrientationChanged(PlatformDeviceOrientation.portraitUp); + } + + // Assert + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + }); + + group('Function Tests', () { + late MockCameraApi mockApi; + late AVFoundationCamera camera; + late int cameraId; + + setUp(() async { + mockApi = MockCameraApi(); + when(mockApi.create(any, any)).thenAnswer((_) async => 1); + camera = AVFoundationCamera(api: mockApi); + cameraId = await camera.createCamera( + const CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + final Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add( + CameraInitializedEvent(cameraId, 1920, 1080, ExposureMode.auto, true, FocusMode.auto, true), + ); + await initializeFuture; + }); + + test('Should fetch CameraDescription instances for available cameras', () async { + final returnData = [ + PlatformCameraDescription( + name: 'Test 1', + lensDirection: PlatformCameraLensDirection.front, + lensType: PlatformCameraLensType.ultraWide, + ), + PlatformCameraDescription( + name: 'Test 2', + lensDirection: PlatformCameraLensDirection.back, + lensType: PlatformCameraLensType.telephoto, + ), + ]; + when(mockApi.getAvailableCameras()).thenAnswer((_) async => returnData); + + final List cameras = await camera.availableCameras(); + + expect(cameras.length, returnData.length); + for (var i = 0; i < returnData.length; i++) { + expect(cameras[i].name, returnData[i].name); + expect( + cameras[i].lensDirection, + cameraLensDirectionFromPlatform(returnData[i].lensDirection), + ); + expect(cameras[i].lensType, cameraLensTypeFromPlatform(returnData[i].lensType)); + // This value isn't provided by the platform, so is hard-coded to 90. + expect(cameras[i].sensorOrientation, 90); + } + }); + + test('Should throw CameraException when availableCameras throws a PlatformException', () { + const code = 'TESTING_ERROR_CODE'; + const message = 'Mock error message used during testing.'; + when( + mockApi.getAvailableCameras(), + ).thenAnswer((_) async => throw PlatformException(code: code, message: message)); + + expect( + camera.availableCameras, + throwsA( + isA() + .having((CameraException e) => e.code, 'code', code) + .having((CameraException e) => e.description, 'description', message), + ), + ); + }); + + test('Should take a picture and return an XFile instance', () async { + const stubPath = '/test/path.jpg'; + when(mockApi.takePicture()).thenAnswer((_) async => stubPath); + + final XFile file = await camera.takePicture(cameraId); + + expect(file.path, stubPath); + }); + + test('Should prepare for video recording', () async { + await camera.prepareForVideoRecording(); + + verify(mockApi.prepareForVideoRecording()); + }); + + test('Should start recording a video', () async { + await camera.startVideoRecording(cameraId); + + verify(mockApi.startVideoRecording(any)); + }); + + test( + 'Should pass enableStream if callback is passed when starting recording a video', + () async { + await camera.startVideoCapturing( + VideoCaptureOptions(cameraId, streamCallback: (CameraImageData imageData) {}), + ); + + verify(mockApi.startVideoRecording(true)); + }, + ); + + test('Should stop a video recording and return the file', () async { + const stubPath = '/test/path.mp4'; + when(mockApi.stopVideoRecording()).thenAnswer((_) async => stubPath); + + final XFile file = await camera.stopVideoRecording(cameraId); + + expect(file.path, stubPath); + }); + + test('Should pause a video recording', () async { + await camera.pauseVideoRecording(cameraId); + + verify(mockApi.pauseVideoRecording()); + }); + + test('Should resume a video recording', () async { + await camera.resumeVideoRecording(cameraId); + + verify(mockApi.resumeVideoRecording()); + }); + + test('Should set the description while recording', () async { + const camera2Description = CameraDescription( + name: 'Test2', + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ); + + await camera.setDescriptionWhileRecording(camera2Description); + + verify(mockApi.updateDescriptionWhileRecording(camera2Description.name)); + }); + + test('Should set the flash mode to torch', () async { + await camera.setFlashMode(cameraId, FlashMode.torch); + + verify(mockApi.setFlashMode(PlatformFlashMode.torch)); + }); + + test('Should set the flash mode to always', () async { + await camera.setFlashMode(cameraId, FlashMode.always); + + verify(mockApi.setFlashMode(PlatformFlashMode.always)); + }); + + test('Should set the flash mode to auto', () async { + await camera.setFlashMode(cameraId, FlashMode.auto); + + verify(mockApi.setFlashMode(PlatformFlashMode.auto)); + }); + + test('Should set the flash mode to off', () async { + await camera.setFlashMode(cameraId, FlashMode.off); + + verify(mockApi.setFlashMode(PlatformFlashMode.off)); + }); + + test('Should set the exposure mode to auto', () async { + await camera.setExposureMode(cameraId, ExposureMode.auto); + + verify(mockApi.setExposureMode(PlatformExposureMode.auto)); + }); + + test('Should set the exposure mode to locked', () async { + await camera.setExposureMode(cameraId, ExposureMode.locked); + + verify(mockApi.setExposureMode(PlatformExposureMode.locked)); + }); + + test('Should set the exposure point to a value', () async { + const point = Point(0.4, 0.6); + await camera.setExposurePoint(cameraId, point); + + final VerificationResult verification = verify(mockApi.setExposurePoint(captureAny)); + final passedPoint = verification.captured[0] as PlatformPoint?; + expect(passedPoint?.x, point.x); + expect(passedPoint?.y, point.y); + }); + + test('Should set the exposure point to null for reset', () async { + await camera.setExposurePoint(cameraId, null); + + final VerificationResult verification = verify(mockApi.setExposurePoint(captureAny)); + final passedPoint = verification.captured[0] as PlatformPoint?; + expect(passedPoint, null); + }); + + test('Should get the min exposure offset', () async { + const stubMinOffset = 2.0; + when(mockApi.getMinExposureOffset()).thenAnswer((_) async => stubMinOffset); + + final double minExposureOffset = await camera.getMinExposureOffset(cameraId); + + expect(minExposureOffset, stubMinOffset); + }); + + test('Should get the max exposure offset', () async { + const stubMaxOffset = 2.0; + when(mockApi.getMaxExposureOffset()).thenAnswer((_) async => stubMaxOffset); + + final double maxExposureOffset = await camera.getMaxExposureOffset(cameraId); + + expect(maxExposureOffset, stubMaxOffset); + }); + + test('Exposure offset step size should always return zero', () async { + final double stepSize = await camera.getExposureOffsetStepSize(cameraId); + + expect(stepSize, 0.0); + }); + + test('Should set the exposure offset', () async { + const stubOffset = 0.5; + final double actualOffset = await camera.setExposureOffset(cameraId, 0.5); + + verify(mockApi.setExposureOffset(stubOffset)); + // iOS never adjusts the offset. + expect(actualOffset, stubOffset); + }); + + test('Should set the focus mode to auto', () async { + await camera.setFocusMode(cameraId, FocusMode.auto); + + verify(mockApi.setFocusMode(PlatformFocusMode.auto)); + }); + + test('Should set the focus mode to locked', () async { + await camera.setFocusMode(cameraId, FocusMode.locked); + + verify(mockApi.setFocusMode(PlatformFocusMode.locked)); + }); + + test('Should set the focus point to a value', () async { + const point = Point(0.4, 0.6); + await camera.setFocusPoint(cameraId, point); + + final VerificationResult verification = verify(mockApi.setFocusPoint(captureAny)); + final passedPoint = verification.captured[0] as PlatformPoint?; + expect(passedPoint?.x, point.x); + expect(passedPoint?.y, point.y); + }); + + test('Should set the focus point to null for reset', () async { + await camera.setFocusPoint(cameraId, null); + + final VerificationResult verification = verify(mockApi.setFocusPoint(captureAny)); + final passedPoint = verification.captured[0] as PlatformPoint?; + expect(passedPoint, null); + }); + + test('Should build a texture widget as preview widget', () async { + final Widget widget = camera.buildPreview(cameraId); + + expect(widget is Texture, isTrue); + expect((widget as Texture).textureId, cameraId); + }); + + test('Should get the max zoom level', () async { + const stubZoomLevel = 10.0; + when(mockApi.getMaxZoomLevel()).thenAnswer((_) async => stubZoomLevel); + + final double maxZoomLevel = await camera.getMaxZoomLevel(cameraId); + + expect(maxZoomLevel, stubZoomLevel); + }); + + test('Should get the min zoom level', () async { + const stubZoomLevel = 10.0; + when(mockApi.getMinZoomLevel()).thenAnswer((_) async => stubZoomLevel); + + final double minZoomLevel = await camera.getMinZoomLevel(cameraId); + + expect(minZoomLevel, stubZoomLevel); + }); + + test('Should set the zoom level', () async { + const zoom = 2.0; + + await camera.setZoomLevel(cameraId, zoom); + + verify(mockApi.setZoomLevel(zoom)); + }); + + test('Should throw CameraException when illegal zoom level is supplied', () async { + const code = 'ZOOM_ERROR'; + const message = 'Illegal zoom error'; + when( + mockApi.setZoomLevel(any), + ).thenAnswer((_) async => throw PlatformException(code: code, message: message)); + + expect( + () => camera.setZoomLevel(cameraId, -1.0), + throwsA( + isA() + .having((CameraException e) => e.code, 'code', code) + .having((CameraException e) => e.description, 'description', message), + ), + ); + }); + + test('Should set video stabilization mode to off', () async { + when( + mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.off), + ).thenAnswer((_) async => true); + + await camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.off); + + verify(mockApi.setVideoStabilizationMode(PlatformVideoStabilizationMode.off)); + }); + + test('Should set video stabilization mode to level1', () async { + when( + mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.standard), + ).thenAnswer((_) async => true); + + await camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level1); + + verify(mockApi.setVideoStabilizationMode(PlatformVideoStabilizationMode.standard)); + }); + + test('Should set video stabilization mode to cinematic', () async { + when( + mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.cinematic), + ).thenAnswer((_) async => true); + + await camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level2); + + verify(mockApi.setVideoStabilizationMode(PlatformVideoStabilizationMode.cinematic)); + }); + + test('Should set video stabilization mode to cinematicExtended', () async { + when( + mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.cinematicExtended), + ).thenAnswer((_) async => true); + + await camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level3); + + verify(mockApi.setVideoStabilizationMode(PlatformVideoStabilizationMode.cinematicExtended)); + }); + + test('Should get no video stabilization mode', () async { + when(mockApi.isVideoStabilizationModeSupported(any)).thenAnswer((_) async => false); + + final Iterable modes = await camera + .getSupportedVideoStabilizationModes(cameraId); + + expect(modes, isEmpty); + }); + + test('Should get off and standard video stabilization modes', () async { + when( + mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.off), + ).thenAnswer((_) async => true); + when( + mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.standard), + ).thenAnswer((_) async => true); + when( + mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.cinematic), + ).thenAnswer((_) async => false); + when( + mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.cinematicExtended), + ).thenAnswer((_) async => false); + + final List modes = (await camera.getSupportedVideoStabilizationModes( + cameraId, + )).toList(); + + expect(modes, [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + ]); + }); + + test('Should get all video stabilization modes', () async { + when(mockApi.isVideoStabilizationModeSupported(any)).thenAnswer((_) async => true); + + final List modes = (await camera.getSupportedVideoStabilizationModes( + cameraId, + )).toList(); + + expect(modes, [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + VideoStabilizationMode.level2, + VideoStabilizationMode.level3, + ]); + }); + + test('Should throw ArgumentError when unavailable video stabilization mode is set', () async { + when(mockApi.isVideoStabilizationModeSupported(any)).thenAnswer((_) async => false); + + expect( + () => camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.off), + throwsA(isA().having((ArgumentError e) => e.name, 'name', 'mode')), + ); + expect( + () => camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level1), + throwsA(isA().having((ArgumentError e) => e.name, 'name', 'mode')), + ); + expect( + () => camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level2), + throwsA(isA().having((ArgumentError e) => e.name, 'name', 'mode')), + ); + expect( + () => camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level3), + throwsA(isA().having((ArgumentError e) => e.name, 'name', 'mode')), + ); + }); + + test('Should throw CameraException when illegal zoom level is supplied', () async { + const code = 'ZOOM_ERROR'; + const message = 'Illegal zoom error'; + when( + mockApi.setZoomLevel(any), + ).thenAnswer((_) async => throw PlatformException(code: code, message: message)); + + expect( + () => camera.setZoomLevel(cameraId, -1.0), + throwsA( + isA() + .having((CameraException e) => e.code, 'code', code) + .having((CameraException e) => e.description, 'description', message), + ), + ); + }); + + test('Should lock the capture orientation', () async { + await camera.lockCaptureOrientation(cameraId, DeviceOrientation.portraitUp); + + verify(mockApi.lockCaptureOrientation(PlatformDeviceOrientation.portraitUp)); + }); + + test('Should unlock the capture orientation', () async { + await camera.unlockCaptureOrientation(cameraId); + + verify(mockApi.unlockCaptureOrientation()); + }); + + test('Should pause the camera preview', () async { + await camera.pausePreview(cameraId); + + verify(mockApi.pausePreview()); + }); + + test('Should resume the camera preview', () async { + await camera.resumePreview(cameraId); + + verify(mockApi.resumePreview()); + }); + + test('Should report support for image streaming', () async { + expect(camera.supportsImageStreaming(), true); + }); + + test('Should start streaming', () async { + final StreamSubscription subscription = camera + .onStreamedFrameAvailable(cameraId) + .listen((CameraImageData imageData) {}); + + verify(mockApi.startImageStream()); + + await subscription.cancel(); + }); + + test('Should stop streaming', () async { + final StreamSubscription subscription = camera + .onStreamedFrameAvailable(cameraId) + .listen((CameraImageData imageData) {}); + await subscription.cancel(); + + verify(mockApi.startImageStream()); + verify(mockApi.stopImageStream()); + }); + + test('Should set the ImageFileFormat to heif', () async { + await camera.setImageFileFormat(cameraId, ImageFileFormat.heif); + + verify(mockApi.setImageFileFormat(PlatformImageFileFormat.heif)); + }); + + test('Should set the ImageFileFormat to jpeg', () async { + await camera.setImageFileFormat(cameraId, ImageFileFormat.jpeg); + + verify(mockApi.setImageFileFormat(PlatformImageFileFormat.jpeg)); + }); + + test('Should set the image quality', () async { + await camera.setJpegImageQuality(cameraId, 50); + + verify(mockApi.setJpegImageQuality(50)); + }); + }); +} diff --git a/camera_avfoundation/test/avfoundation_camera_test.mocks.dart b/camera_avfoundation/test/avfoundation_camera_test.mocks.dart new file mode 100644 index 0000000..3a63477 --- /dev/null +++ b/camera_avfoundation/test/avfoundation_camera_test.mocks.dart @@ -0,0 +1,354 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in camera_avfoundation/test/avfoundation_camera_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i4; + +import 'package:camera_avfoundation/src/messages.g.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i3; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member + +/// A class which mocks [CameraApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCameraApi extends _i1.Mock implements _i2.CameraApi { + @override + String get pigeonVar_messageChannelSuffix => + (super.noSuchMethod( + Invocation.getter(#pigeonVar_messageChannelSuffix), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#pigeonVar_messageChannelSuffix), + ), + returnValueForMissingStub: _i3.dummyValue( + this, + Invocation.getter(#pigeonVar_messageChannelSuffix), + ), + ) + as String); + + @override + _i4.Future> getAvailableCameras() => + (super.noSuchMethod( + Invocation.method(#getAvailableCameras, []), + returnValue: _i4.Future>.value( + <_i2.PlatformCameraDescription>[], + ), + returnValueForMissingStub: _i4.Future>.value( + <_i2.PlatformCameraDescription>[], + ), + ) + as _i4.Future>); + + @override + _i4.Future create(String? cameraName, _i2.PlatformMediaSettings? settings) => + (super.noSuchMethod( + Invocation.method(#create, [cameraName, settings]), + returnValue: _i4.Future.value(0), + returnValueForMissingStub: _i4.Future.value(0), + ) + as _i4.Future); + + @override + _i4.Future initialize(int? cameraId, _i2.PlatformImageFormatGroup? imageFormat) => + (super.noSuchMethod( + Invocation.method(#initialize, [cameraId, imageFormat]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future startImageStream() => + (super.noSuchMethod( + Invocation.method(#startImageStream, []), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future stopImageStream() => + (super.noSuchMethod( + Invocation.method(#stopImageStream, []), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future receivedImageStreamData() => + (super.noSuchMethod( + Invocation.method(#receivedImageStreamData, []), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future dispose(int? cameraId) => + (super.noSuchMethod( + Invocation.method(#dispose, [cameraId]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future lockCaptureOrientation(_i2.PlatformDeviceOrientation? orientation) => + (super.noSuchMethod( + Invocation.method(#lockCaptureOrientation, [orientation]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future unlockCaptureOrientation() => + (super.noSuchMethod( + Invocation.method(#unlockCaptureOrientation, []), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future takePicture() => + (super.noSuchMethod( + Invocation.method(#takePicture, []), + returnValue: _i4.Future.value( + _i3.dummyValue(this, Invocation.method(#takePicture, [])), + ), + returnValueForMissingStub: _i4.Future.value( + _i3.dummyValue(this, Invocation.method(#takePicture, [])), + ), + ) + as _i4.Future); + + @override + _i4.Future prepareForVideoRecording() => + (super.noSuchMethod( + Invocation.method(#prepareForVideoRecording, []), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future startVideoRecording(bool? enableStream) => + (super.noSuchMethod( + Invocation.method(#startVideoRecording, [enableStream]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future stopVideoRecording() => + (super.noSuchMethod( + Invocation.method(#stopVideoRecording, []), + returnValue: _i4.Future.value( + _i3.dummyValue(this, Invocation.method(#stopVideoRecording, [])), + ), + returnValueForMissingStub: _i4.Future.value( + _i3.dummyValue(this, Invocation.method(#stopVideoRecording, [])), + ), + ) + as _i4.Future); + + @override + _i4.Future pauseVideoRecording() => + (super.noSuchMethod( + Invocation.method(#pauseVideoRecording, []), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future resumeVideoRecording() => + (super.noSuchMethod( + Invocation.method(#resumeVideoRecording, []), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future setFlashMode(_i2.PlatformFlashMode? mode) => + (super.noSuchMethod( + Invocation.method(#setFlashMode, [mode]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future setExposureMode(_i2.PlatformExposureMode? mode) => + (super.noSuchMethod( + Invocation.method(#setExposureMode, [mode]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future setExposurePoint(_i2.PlatformPoint? point) => + (super.noSuchMethod( + Invocation.method(#setExposurePoint, [point]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future getMinExposureOffset() => + (super.noSuchMethod( + Invocation.method(#getMinExposureOffset, []), + returnValue: _i4.Future.value(0.0), + returnValueForMissingStub: _i4.Future.value(0.0), + ) + as _i4.Future); + + @override + _i4.Future getMaxExposureOffset() => + (super.noSuchMethod( + Invocation.method(#getMaxExposureOffset, []), + returnValue: _i4.Future.value(0.0), + returnValueForMissingStub: _i4.Future.value(0.0), + ) + as _i4.Future); + + @override + _i4.Future setExposureOffset(double? offset) => + (super.noSuchMethod( + Invocation.method(#setExposureOffset, [offset]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future setFocusMode(_i2.PlatformFocusMode? mode) => + (super.noSuchMethod( + Invocation.method(#setFocusMode, [mode]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future setFocusPoint(_i2.PlatformPoint? point) => + (super.noSuchMethod( + Invocation.method(#setFocusPoint, [point]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future getMinZoomLevel() => + (super.noSuchMethod( + Invocation.method(#getMinZoomLevel, []), + returnValue: _i4.Future.value(0.0), + returnValueForMissingStub: _i4.Future.value(0.0), + ) + as _i4.Future); + + @override + _i4.Future getMaxZoomLevel() => + (super.noSuchMethod( + Invocation.method(#getMaxZoomLevel, []), + returnValue: _i4.Future.value(0.0), + returnValueForMissingStub: _i4.Future.value(0.0), + ) + as _i4.Future); + + @override + _i4.Future setZoomLevel(double? zoom) => + (super.noSuchMethod( + Invocation.method(#setZoomLevel, [zoom]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future setVideoStabilizationMode(_i2.PlatformVideoStabilizationMode? mode) => + (super.noSuchMethod( + Invocation.method(#setVideoStabilizationMode, [mode]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future isVideoStabilizationModeSupported(_i2.PlatformVideoStabilizationMode? mode) => + (super.noSuchMethod( + Invocation.method(#isVideoStabilizationModeSupported, [mode]), + returnValue: _i4.Future.value(false), + returnValueForMissingStub: _i4.Future.value(false), + ) + as _i4.Future); + + @override + _i4.Future pausePreview() => + (super.noSuchMethod( + Invocation.method(#pausePreview, []), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future resumePreview() => + (super.noSuchMethod( + Invocation.method(#resumePreview, []), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future updateDescriptionWhileRecording(String? cameraName) => + (super.noSuchMethod( + Invocation.method(#updateDescriptionWhileRecording, [cameraName]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future setImageFileFormat(_i2.PlatformImageFileFormat? format) => + (super.noSuchMethod( + Invocation.method(#setImageFileFormat, [format]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future setJpegImageQuality(int? quality) => + (super.noSuchMethod( + Invocation.method(#setJpegImageQuality, [quality]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); +} diff --git a/camera_avfoundation/test/flutter_test_config.dart b/camera_avfoundation/test/flutter_test_config.dart new file mode 100644 index 0000000..d1eb777 --- /dev/null +++ b/camera_avfoundation/test/flutter_test_config.dart @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + +Future testExecutable(FutureOr Function() testMain) async { + LeakTesting.enable(); + LeakTracking.warnForUnsupportedPlatforms = false; + await testMain(); +} diff --git a/camera_avfoundation/test/type_conversion_test.dart b/camera_avfoundation/test/type_conversion_test.dart new file mode 100644 index 0000000..56bea24 --- /dev/null +++ b/camera_avfoundation/test/type_conversion_test.dart @@ -0,0 +1,59 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:camera_avfoundation/src/messages.g.dart'; +import 'package:camera_avfoundation/src/type_conversion.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('CameraImageData can be created', () { + final CameraImageData cameraImage = cameraImageFromPlatformData( + PlatformCameraImageData( + formatCode: 1, + width: 4, + height: 1, + lensAperture: 1.8, + sensorExposureTimeNanoseconds: 9991324, + sensorSensitivity: 92.0, + planes: [ + PlatformCameraImagePlane( + bytes: Uint8List.fromList([1, 2, 3, 4]), + bytesPerRow: 4, + width: 4, + height: 1, + ), + ], + ), + ); + expect(cameraImage.height, 1); + expect(cameraImage.width, 4); + expect(cameraImage.format.group, ImageFormatGroup.unknown); + expect(cameraImage.planes.length, 1); + }); + + test('CameraImageData has ImageFormatGroup.yuv420', () { + final CameraImageData cameraImage = cameraImageFromPlatformData( + PlatformCameraImageData( + formatCode: 875704438, + width: 4, + height: 1, + lensAperture: 1.8, + sensorExposureTimeNanoseconds: 9991324, + sensorSensitivity: 92.0, + planes: [ + PlatformCameraImagePlane( + bytes: Uint8List.fromList([1, 2, 3, 4]), + bytesPerRow: 4, + width: 4, + height: 1, + ), + ], + ), + ); + expect(cameraImage.format.group, ImageFormatGroup.yuv420); + }); +} diff --git a/camera_avfoundation/test/utils_test.dart b/camera_avfoundation/test/utils_test.dart new file mode 100644 index 0000000..6733a34 --- /dev/null +++ b/camera_avfoundation/test/utils_test.dart @@ -0,0 +1,66 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_avfoundation/src/messages.g.dart'; +import 'package:camera_avfoundation/src/utils.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Utility methods', () { + test('Should convert CameraLensDirection values correctly', () { + expect( + cameraLensDirectionFromPlatform(PlatformCameraLensDirection.back), + CameraLensDirection.back, + ); + expect( + cameraLensDirectionFromPlatform(PlatformCameraLensDirection.front), + CameraLensDirection.front, + ); + expect( + cameraLensDirectionFromPlatform(PlatformCameraLensDirection.external), + CameraLensDirection.external, + ); + }); + + test('serializeDeviceOrientation() should serialize correctly', () { + expect( + serializeDeviceOrientation(DeviceOrientation.portraitUp), + PlatformDeviceOrientation.portraitUp, + ); + expect( + serializeDeviceOrientation(DeviceOrientation.portraitDown), + PlatformDeviceOrientation.portraitDown, + ); + expect( + serializeDeviceOrientation(DeviceOrientation.landscapeRight), + PlatformDeviceOrientation.landscapeRight, + ); + expect( + serializeDeviceOrientation(DeviceOrientation.landscapeLeft), + PlatformDeviceOrientation.landscapeLeft, + ); + }); + + test('deviceOrientationFromPlatform() should convert correctly', () { + expect( + deviceOrientationFromPlatform(PlatformDeviceOrientation.portraitUp), + DeviceOrientation.portraitUp, + ); + expect( + deviceOrientationFromPlatform(PlatformDeviceOrientation.portraitDown), + DeviceOrientation.portraitDown, + ); + expect( + deviceOrientationFromPlatform(PlatformDeviceOrientation.landscapeRight), + DeviceOrientation.landscapeRight, + ); + expect( + deviceOrientationFromPlatform(PlatformDeviceOrientation.landscapeLeft), + DeviceOrientation.landscapeLeft, + ); + }); + }); +}