add new dep
This commit is contained in:
parent
c3d0858823
commit
95853567e9
89 changed files with 14794 additions and 1 deletions
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
67
camera_avfoundation/AUTHORS
Normal file
67
camera_avfoundation/AUTHORS
Normal file
|
|
@ -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 <email address>
|
||||
|
||||
Google Inc.
|
||||
The Chromium Authors
|
||||
German Saprykin <saprykin.h@gmail.com>
|
||||
Benjamin Sauer <sauer.benjamin@gmail.com>
|
||||
larsenthomasj@gmail.com
|
||||
Ali Bitek <alibitek@protonmail.ch>
|
||||
Pol Batlló <pol.batllo@gmail.com>
|
||||
Anatoly Pulyaevskiy
|
||||
Hayden Flinner <haydenflinner@gmail.com>
|
||||
Stefano Rodriguez <hlsroddy@gmail.com>
|
||||
Salvatore Giordano <salvatoregiordanoo@gmail.com>
|
||||
Brian Armstrong <brian@flutter.institute>
|
||||
Paul DeMarco <paulmdemarco@gmail.com>
|
||||
Fabricio Nogueira <feufeu@gmail.com>
|
||||
Simon Lightfoot <simon@devangels.london>
|
||||
Ashton Thomas <ashton@acrinta.com>
|
||||
Thomas Danner <thmsdnnr@gmail.com>
|
||||
Diego Velásquez <diego.velasquez.lopez@gmail.com>
|
||||
Hajime Nakamura <nkmrhj@gmail.com>
|
||||
Tuyển Vũ Xuân <netsoft1985@gmail.com>
|
||||
Miguel Ruivo <miguel@miguelruivo.com>
|
||||
Sarthak Verma <sarthak@artiosys.com>
|
||||
Mike Diarmid <mike@invertase.io>
|
||||
Invertase <oss@invertase.io>
|
||||
Elliot Hesp <elliot@invertase.io>
|
||||
Vince Varga <vince.varga@smaho.com>
|
||||
Aawaz Gyawali <awazgyawali@gmail.com>
|
||||
EUI Limited <ian.evans3@admiralgroup.co.uk>
|
||||
Katarina Sheremet <katarina@sheremet.ch>
|
||||
Thomas Stockx <thomas@stockxit.com>
|
||||
Sarbagya Dhaubanjar <sarbagyastha@gmail.com>
|
||||
Ozkan Eksi <ozeksi@gmail.com>
|
||||
Rishab Nayak <rishab@bu.edu>
|
||||
ko2ic <ko2ic.dev@gmail.com>
|
||||
Jonathan Younger <jonathan@daikini.com>
|
||||
Jose Sanchez <josesm82@gmail.com>
|
||||
Debkanchan Samadder <debu.samadder@gmail.com>
|
||||
Audrius Karosevicius <audrius.karosevicius@gmail.com>
|
||||
Lukasz Piliszczuk <lukasz@intheloup.io>
|
||||
SoundReply Solutions GmbH <ch@soundreply.com>
|
||||
Rafal Wachol <rwachol@gmail.com>
|
||||
Pau Picas <pau.picas@gmail.com>
|
||||
Christian Weder <chrstian.weder@yapeal.ch>
|
||||
Alexandru Tuca <salexandru.tuca@outlook.com>
|
||||
Christian Weder <chrstian.weder@yapeal.ch>
|
||||
Rhodes Davis Jr. <rody.davis.jr@gmail.com>
|
||||
Luigi Agosti <luigi@tengio.com>
|
||||
Quentin Le Guennec <quentin@tengio.com>
|
||||
Koushik Ravikumar <koushik@tengio.com>
|
||||
Nissim Dsilva <nissim@tengio.com>
|
||||
Giancarlo Rocha <giancarloiff@gmail.com>
|
||||
Ryo Miyake <ryo@miyake.id>
|
||||
Théo Champion <contact.theochampion@gmail.com>
|
||||
Kazuki Yamaguchi <y.kazuki0614n@gmail.com>
|
||||
Eitan Schwartz <eshvartz@gmail.com>
|
||||
Chris Rutkowski <chrisrutkowski89@gmail.com>
|
||||
Juan Alvarez <juan.alvarez@resideo.com>
|
||||
Aleksandr Yurkovskiy <sanekyy@gmail.com>
|
||||
Anton Borries <mail@antonborri.es>
|
||||
Alex Li <google@alexv525.com>
|
||||
Rahul Raj <64.rahulraj@gmail.com>
|
||||
Rui Craveiro <ruicraveiro@squarealfa.com>
|
||||
446
camera_avfoundation/CHANGELOG.md
Normal file
446
camera_avfoundation/CHANGELOG.md
Normal file
|
|
@ -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.
|
||||
25
camera_avfoundation/LICENSE
Normal file
25
camera_avfoundation/LICENSE
Normal file
|
|
@ -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.
|
||||
15
camera_avfoundation/README.md
Normal file
15
camera_avfoundation/README.md
Normal file
|
|
@ -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
|
||||
11
camera_avfoundation/TWONLY_CHANGES.md
Normal file
11
camera_avfoundation/TWONLY_CHANGES.md
Normal file
|
|
@ -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.
|
||||
138
camera_avfoundation/darwin/Tests/AvailableCamerasTests.swift
Normal file
138
camera_avfoundation/darwin/Tests/AvailableCamerasTests.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
146
camera_avfoundation/darwin/Tests/CameraExposureTests.swift
Normal file
146
camera_avfoundation/darwin/Tests/CameraExposureTests.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
168
camera_avfoundation/darwin/Tests/CameraOrientationTests.swift
Normal file
168
camera_avfoundation/darwin/Tests/CameraOrientationTests.swift
Normal file
|
|
@ -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<Void>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
237
camera_avfoundation/darwin/Tests/CameraPermissionTests.swift
Normal file
237
camera_avfoundation/darwin/Tests/CameraPermissionTests.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
73
camera_avfoundation/darwin/Tests/CameraPropertiesTests.swift
Normal file
73
camera_avfoundation/darwin/Tests/CameraPropertiesTests.swift
Normal file
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
168
camera_avfoundation/darwin/Tests/CameraSetFlashModeTests.swift
Normal file
168
camera_avfoundation/darwin/Tests/CameraSetFlashModeTests.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
166
camera_avfoundation/darwin/Tests/CameraSetFocusModeTests.swift
Normal file
166
camera_avfoundation/darwin/Tests/CameraSetFocusModeTests.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
356
camera_avfoundation/darwin/Tests/CameraSettingsTests.swift
Normal file
356
camera_avfoundation/darwin/Tests/CameraSettingsTests.swift
Normal file
|
|
@ -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."
|
||||
)
|
||||
}
|
||||
}
|
||||
219
camera_avfoundation/darwin/Tests/CameraTestUtils.swift
Normal file
219
camera_avfoundation/darwin/Tests/CameraTestUtils.swift
Normal file
|
|
@ -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<T>(
|
||||
_ result: Result<T, any Error>, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
111
camera_avfoundation/darwin/Tests/CameraZoomTests.swift
Normal file
111
camera_avfoundation/darwin/Tests/CameraZoomTests.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
33
camera_avfoundation/darwin/Tests/Mocks/MockAssetWriter.swift
Normal file
33
camera_avfoundation/darwin/Tests/Mocks/MockAssetWriter.swift
Normal file
|
|
@ -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) {}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
238
camera_avfoundation/darwin/Tests/Mocks/MockCamera.swift
Normal file
238
camera_avfoundation/darwin/Tests/Mocks/MockCamera.swift
Normal file
|
|
@ -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, any Error>) -> Void, FlutterBinaryMessenger?) -> Void)?
|
||||
var pauseVideoRecordingStub: (() -> Void)?
|
||||
var resumeVideoRecordingStub: (() -> Void)?
|
||||
var stopVideoRecordingStub: ((@escaping (Result<String, any Error>) -> Void) -> Void)?
|
||||
var captureToFileStub: ((@escaping (Result<String, any Error>) -> 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, any Error>) -> Void) -> Void)?
|
||||
var setFocusModeStub: ((PlatformFocusMode) -> Void)?
|
||||
var setFocusPointStub: ((PlatformPoint?, @escaping (Result<Void, any Error>) -> Void) -> Void)?
|
||||
var setZoomLevelStub: ((CGFloat, @escaping (Result<Void, any Error>) -> Void) -> Void)?
|
||||
var setFlashModeStub: ((PlatformFlashMode, @escaping (Result<Void, any Error>) -> Void) -> Void)?
|
||||
var pausePreviewStub: (() -> Void)?
|
||||
var resumePreviewStub: (() -> Void)?
|
||||
var setDescriptionWhileRecordingStub:
|
||||
((String, @escaping (Result<Void, any Error>) -> Void) -> Void)?
|
||||
var startImageStreamStub:
|
||||
((FlutterBinaryMessenger, @escaping (Result<Void, any Error>) -> Void) -> Void)?
|
||||
var stopImageStreamStub: (() -> Void)?
|
||||
var setVideoStabilizationModeStub:
|
||||
((PlatformVideoStabilizationMode, @escaping (Result<Void, any Error>) -> 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, any Error>) -> Void,
|
||||
messengerForStreaming messenger: FlutterBinaryMessenger?
|
||||
) {
|
||||
startVideoRecordingStub?(completion, messenger)
|
||||
}
|
||||
|
||||
func pauseVideoRecording() {
|
||||
pauseVideoRecordingStub?()
|
||||
}
|
||||
|
||||
func resumeVideoRecording() {
|
||||
resumeVideoRecordingStub?()
|
||||
}
|
||||
|
||||
func stopVideoRecording(completion: @escaping (Result<String, any Error>) -> Void) {
|
||||
stopVideoRecordingStub?(completion)
|
||||
}
|
||||
|
||||
func captureToFile(completion: @escaping (Result<String, any Error>) -> 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, any Error>) -> Void
|
||||
) {
|
||||
setExposurePointStub?(point, withCompletion)
|
||||
}
|
||||
|
||||
func setFocusMode(_ mode: PlatformFocusMode) {
|
||||
setFocusModeStub?(mode)
|
||||
}
|
||||
|
||||
func setFocusPoint(
|
||||
_ point: PlatformPoint?, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
setFocusPointStub?(point, completion)
|
||||
}
|
||||
|
||||
func setZoomLevel(
|
||||
_ zoom: CGFloat,
|
||||
withCompletion completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
setZoomLevelStub?(zoom, completion)
|
||||
}
|
||||
|
||||
func setFlashMode(
|
||||
_ mode: PlatformFlashMode,
|
||||
withCompletion completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
setFlashModeStub?(mode, completion)
|
||||
}
|
||||
|
||||
func pausePreview() {
|
||||
pausePreviewStub?()
|
||||
}
|
||||
|
||||
func resumePreview() {
|
||||
resumePreviewStub?()
|
||||
}
|
||||
|
||||
func setVideoStabilizationMode(
|
||||
_ mode: PlatformVideoStabilizationMode,
|
||||
withCompletion completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
setVideoStabilizationModeStub?(mode, completion)
|
||||
}
|
||||
|
||||
func isVideoStabilizationModeSupported(_ mode: PlatformVideoStabilizationMode) -> Bool {
|
||||
return getIsVideoStabilizationModeSupportedStub?(mode) ?? false
|
||||
}
|
||||
|
||||
func setDescriptionWhileRecording(
|
||||
_ cameraName: String,
|
||||
withCompletion completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
setDescriptionWhileRecordingStub?(cameraName, completion)
|
||||
}
|
||||
|
||||
func startImageStream(
|
||||
with messenger: FlutterBinaryMessenger,
|
||||
completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
startImageStreamStub?(messenger, completion)
|
||||
}
|
||||
|
||||
func stopImageStream() {
|
||||
stopImageStreamStub?()
|
||||
}
|
||||
|
||||
func captureOutput(
|
||||
_ output: AVCaptureOutput,
|
||||
didOutput sampleBuffer: CMSampleBuffer,
|
||||
from connection: AVCaptureConnection
|
||||
) {}
|
||||
|
||||
func close() {}
|
||||
|
||||
func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -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) ?? []
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
127
camera_avfoundation/darwin/Tests/Mocks/MockCaptureDevice.swift
Normal file
127
camera_avfoundation/darwin/Tests/Mocks/MockCaptureDevice.swift
Normal file
|
|
@ -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() {}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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] = []
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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?
|
||||
) {}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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, PigeonError>) -> Void
|
||||
) {
|
||||
deviceOrientationChangedCalled = true
|
||||
lastOrientation = orientationArg
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
248
camera_avfoundation/darwin/Tests/PhotoCaptureTests.swift
Normal file
248
camera_avfoundation/darwin/Tests/PhotoCaptureTests.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
35
camera_avfoundation/darwin/Tests/QueueUtilsTests.swift
Normal file
35
camera_avfoundation/darwin/Tests/QueueUtilsTests.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
542
camera_avfoundation/darwin/Tests/SampleBufferTests.swift
Normal file
542
camera_avfoundation/darwin/Tests/SampleBufferTests.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Void>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
217
camera_avfoundation/darwin/Tests/StreamingTests.swift
Normal file
217
camera_avfoundation/darwin/Tests/StreamingTests.swift
Normal file
|
|
@ -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<PlatformCameraImageData> {
|
||||
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<PlatformCameraImageData>? {
|
||||
set {
|
||||
}
|
||||
get {
|
||||
return mockEventSink
|
||||
}
|
||||
}
|
||||
|
||||
var eventSinkSuccessStub: ((PlatformCameraImageData?) -> Void)? {
|
||||
set {
|
||||
mockEventSink.eventSinkSuccessStub = newValue
|
||||
}
|
||||
get {
|
||||
return mockEventSink.eventSinkSuccessStub
|
||||
}
|
||||
}
|
||||
|
||||
override func onListen(
|
||||
withArguments arguments: Any?, sink: PigeonEventSink<PlatformCameraImageData>
|
||||
) {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
27
camera_avfoundation/darwin/camera_avfoundation.podspec
Normal file
27
camera_avfoundation/darwin/camera_avfoundation.podspec
Normal file
|
|
@ -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
|
||||
28
camera_avfoundation/darwin/camera_avfoundation/Package.swift
Normal file
28
camera_avfoundation/darwin/camera_avfoundation/Package.swift
Normal file
|
|
@ -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")
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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, any Error>) -> Void,
|
||||
messengerForStreaming: FlutterBinaryMessenger?
|
||||
)
|
||||
func pauseVideoRecording()
|
||||
func resumeVideoRecording()
|
||||
func stopVideoRecording(completion: @escaping (Result<String, any Error>) -> Void)
|
||||
|
||||
func captureToFile(completion: @escaping (Result<String, any Error>) -> 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, any Error>) -> 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, any Error>) -> Void
|
||||
)
|
||||
|
||||
func setZoomLevel(_ zoom: CGFloat, withCompletion: @escaping (Result<Void, any Error>) -> Void)
|
||||
|
||||
func setVideoStabilizationMode(
|
||||
_ mode: PlatformVideoStabilizationMode,
|
||||
withCompletion: @escaping (Result<Void, any Error>) -> Void)
|
||||
|
||||
func isVideoStabilizationModeSupported(_ mode: PlatformVideoStabilizationMode) -> Bool
|
||||
|
||||
func setFlashMode(
|
||||
_ mode: PlatformFlashMode,
|
||||
withCompletion: @escaping (Result<Void, any Error>) -> Void
|
||||
)
|
||||
|
||||
func pausePreview()
|
||||
func resumePreview()
|
||||
|
||||
func setDescriptionWhileRecording(
|
||||
_ cameraName: String,
|
||||
withCompletion: @escaping (Result<Void, any Error>) -> Void
|
||||
)
|
||||
|
||||
func startImageStream(
|
||||
with: FlutterBinaryMessenger, completion: @escaping (Result<Void, any Error>) -> Void)
|
||||
func stopImageStream()
|
||||
|
||||
// Override to make `AVCaptureVideoDataOutputSampleBufferDelegate`/
|
||||
// `AVCaptureAudioDataOutputSampleBufferDelegate` method non optional
|
||||
override func captureOutput(
|
||||
_ output: AVCaptureOutput,
|
||||
didOutput sampleBuffer: CMSampleBuffer,
|
||||
from connection: AVCaptureConnection
|
||||
)
|
||||
|
||||
func close()
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Int64, any Error>) -> 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<Int64, any Error>) -> 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<Int64, any Error>) -> 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, any Error>) -> 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, any Error>) -> 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, any Error>) -> 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, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.stopImageStream()
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func receivedImageStreamData(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.receivedImageStreamData()
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func dispose(cameraId: Int64, completion: @escaping (Result<Void, any Error>) -> 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, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.lockCaptureOrientation(orientation)
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func unlockCaptureOrientation(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.unlockCaptureOrientation()
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func takePicture(completion: @escaping (Result<String, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.captureToFile(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func prepareForVideoRecording(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setUpCaptureSessionForAudioIfNeeded()
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func startVideoRecording(
|
||||
enableStream: Bool, completion: @escaping (Result<Void, any Error>) -> 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<String, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.stopVideoRecording(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func pauseVideoRecording(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.pauseVideoRecording()
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func resumeVideoRecording(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.resumeVideoRecording()
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func setFlashMode(
|
||||
mode: PlatformFlashMode, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setFlashMode(mode, withCompletion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func setExposureMode(
|
||||
mode: PlatformExposureMode, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setExposureMode(mode)
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func setExposurePoint(
|
||||
point: PlatformPoint?, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setExposurePoint(point, withCompletion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func getMinExposureOffset(completion: @escaping (Result<Double, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
if let minOffset = self?.camera?.minimumExposureOffset {
|
||||
completion(.success(minOffset))
|
||||
} else {
|
||||
completion(.success(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getMaxExposureOffset(completion: @escaping (Result<Double, any Error>) -> 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, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setExposureOffset(offset)
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func setFocusMode(
|
||||
mode: PlatformFocusMode, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setFocusMode(mode)
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func setFocusPoint(point: PlatformPoint?, completion: @escaping (Result<Void, any Error>) -> Void)
|
||||
{
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setFocusPoint(point, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func getMinZoomLevel(completion: @escaping (Result<Double, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
if let minZoom = self?.camera?.minimumAvailableZoomFactor {
|
||||
completion(.success(minZoom))
|
||||
} else {
|
||||
completion(.success(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getMaxZoomLevel(completion: @escaping (Result<Double, any Error>) -> 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, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setZoomLevel(zoom, withCompletion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func setVideoStabilizationMode(
|
||||
mode: PlatformVideoStabilizationMode, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setVideoStabilizationMode(mode, withCompletion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func isVideoStabilizationModeSupported(
|
||||
mode: PlatformVideoStabilizationMode,
|
||||
completion: @escaping (Result<Bool, any Error>) -> 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, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.pausePreview()
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func resumePreview(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.resumePreview()
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func updateDescriptionWhileRecording(
|
||||
cameraName: String, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setDescriptionWhileRecording(cameraName, withCompletion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func setImageFileFormat(
|
||||
format: PlatformImageFileFormat, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setImageFileFormat(format)
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func setJpegImageQuality(
|
||||
quality: Int64, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setJpegImageQuality(quality)
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<PlatformCameraImageData>? { get set }
|
||||
}
|
||||
|
||||
/// Default implementation of ImageStreamHandler.
|
||||
class DefaultImageStreamHandler: ImageDataStreamStreamHandler, ImageStreamHandler {
|
||||
let captureSessionQueue: DispatchQueue
|
||||
var eventSink: PigeonEventSink<PlatformCameraImageData>?
|
||||
|
||||
/// 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<PlatformCameraImageData>
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.eventSink = sink
|
||||
}
|
||||
}
|
||||
|
||||
override func onCancel(withArguments arguments: Any?) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.eventSink = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
5
camera_avfoundation/lib/camera_avfoundation.dart
Normal file
5
camera_avfoundation/lib/camera_avfoundation.dart
Normal file
|
|
@ -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';
|
||||
650
camera_avfoundation/lib/src/avfoundation_camera.dart
Normal file
650
camera_avfoundation/lib/src/avfoundation_camera.dart
Normal file
|
|
@ -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<CameraEvent> cameraEventStreamController =
|
||||
StreamController<CameraEvent>.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<int, HostCameraMessageHandler> hostCameraHandlers = <int, HostCameraMessageHandler>{};
|
||||
|
||||
// The stream to receive frames from the native code.
|
||||
StreamSubscription<dynamic>? _platformImageStreamSubscription;
|
||||
|
||||
// The stream for vending frames to platform interface clients.
|
||||
StreamController<CameraImageData>? _frameStreamController;
|
||||
|
||||
Stream<CameraEvent> _cameraEvents(int cameraId) =>
|
||||
cameraEventStreamController.stream.where((CameraEvent event) => event.cameraId == cameraId);
|
||||
|
||||
@override
|
||||
Future<List<CameraDescription>> availableCameras() async {
|
||||
try {
|
||||
return (await _hostApi.getAvailableCameras()).map(cameraDescriptionFromPlatform).toList();
|
||||
} on PlatformException catch (e) {
|
||||
throw CameraException(e.code, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> createCamera(
|
||||
CameraDescription cameraDescription,
|
||||
ResolutionPreset? resolutionPreset, {
|
||||
bool enableAudio = false,
|
||||
}) => createCameraWithSettings(
|
||||
cameraDescription,
|
||||
MediaSettings(resolutionPreset: resolutionPreset, enableAudio: enableAudio),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<int> 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<void> initializeCamera(
|
||||
int cameraId, {
|
||||
ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown,
|
||||
}) async {
|
||||
hostCameraHandlers.putIfAbsent(
|
||||
cameraId,
|
||||
() => HostCameraMessageHandler(cameraId, cameraEventStreamController),
|
||||
);
|
||||
|
||||
final completer = Completer<void>();
|
||||
|
||||
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<void> dispose(int cameraId) async {
|
||||
final HostCameraMessageHandler? handler = hostCameraHandlers.remove(cameraId);
|
||||
handler?.dispose();
|
||||
|
||||
await _hostApi.dispose(cameraId);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<CameraInitializedEvent> onCameraInitialized(int cameraId) {
|
||||
return _cameraEvents(cameraId).whereType<CameraInitializedEvent>();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<CameraResolutionChangedEvent> onCameraResolutionChanged(int cameraId) {
|
||||
return _cameraEvents(cameraId).whereType<CameraResolutionChangedEvent>();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<CameraClosingEvent> onCameraClosing(int cameraId) {
|
||||
return _cameraEvents(cameraId).whereType<CameraClosingEvent>();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<CameraErrorEvent> onCameraError(int cameraId) {
|
||||
return _cameraEvents(cameraId).whereType<CameraErrorEvent>();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<VideoRecordedEvent> onVideoRecordedEvent(int cameraId) {
|
||||
return _cameraEvents(cameraId).whereType<VideoRecordedEvent>();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<DeviceOrientationChangedEvent> onDeviceOrientationChanged() {
|
||||
return hostHandler.deviceEventStreamController.stream
|
||||
.whereType<DeviceOrientationChangedEvent>();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> lockCaptureOrientation(int cameraId, DeviceOrientation orientation) async {
|
||||
await _hostApi.lockCaptureOrientation(serializeDeviceOrientation(orientation));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> unlockCaptureOrientation(int cameraId) async {
|
||||
await _hostApi.unlockCaptureOrientation();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<XFile> takePicture(int cameraId) async {
|
||||
final String path = await _hostApi.takePicture();
|
||||
return XFile(path);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> prepareForVideoRecording() async {
|
||||
await _hostApi.prepareForVideoRecording();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startVideoRecording(int cameraId, {Duration? maxVideoDuration}) async {
|
||||
// Ignore maxVideoDuration, as it is unimplemented and deprecated.
|
||||
return startVideoCapturing(VideoCaptureOptions(cameraId));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> 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<XFile> stopVideoRecording(int cameraId) async {
|
||||
final String path = await _hostApi.stopVideoRecording();
|
||||
return XFile(path);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pauseVideoRecording(int cameraId) async {
|
||||
await _hostApi.pauseVideoRecording();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> resumeVideoRecording(int cameraId) async {
|
||||
await _hostApi.resumeVideoRecording();
|
||||
}
|
||||
|
||||
@override
|
||||
bool supportsImageStreaming() => true;
|
||||
|
||||
@override
|
||||
Stream<CameraImageData> onStreamedFrameAvailable(
|
||||
int cameraId, {
|
||||
CameraImageStreamOptions? options,
|
||||
}) {
|
||||
_frameStreamController = _createStreamController(onListen: _onFrameStreamListen);
|
||||
return _frameStreamController!.stream;
|
||||
}
|
||||
|
||||
StreamController<CameraImageData> _createStreamController({void Function()? onListen}) {
|
||||
return StreamController<CameraImageData>(
|
||||
onListen: onListen ?? () {},
|
||||
onPause: _onFrameStreamPauseResume,
|
||||
onResume: _onFrameStreamPauseResume,
|
||||
onCancel: _onFrameStreamCancel,
|
||||
);
|
||||
}
|
||||
|
||||
void _onFrameStreamListen() {
|
||||
_startPlatformStream();
|
||||
}
|
||||
|
||||
Future<void> _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<void> _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<void> setFlashMode(int cameraId, FlashMode mode) async {
|
||||
await _hostApi.setFlashMode(_pigeonFlashMode(mode));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setExposureMode(int cameraId, ExposureMode mode) async {
|
||||
await _hostApi.setExposureMode(_pigeonExposureMode(mode));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setExposurePoint(int cameraId, Point<double>? 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<double> getMinExposureOffset(int cameraId) async {
|
||||
return _hostApi.getMinExposureOffset();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> getMaxExposureOffset(int cameraId) async {
|
||||
return _hostApi.getMaxExposureOffset();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> getExposureOffsetStepSize(int cameraId) async {
|
||||
// iOS has no step size.
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> 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<void> setFocusMode(int cameraId, FocusMode mode) async {
|
||||
await _hostApi.setFocusMode(_pigeonFocusMode(mode));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setFocusPoint(int cameraId, Point<double>? 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<double> getMaxZoomLevel(int cameraId) async {
|
||||
return _hostApi.getMaxZoomLevel();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> getMinZoomLevel(int cameraId) async {
|
||||
return _hostApi.getMinZoomLevel();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setZoomLevel(int cameraId, double zoom) async {
|
||||
try {
|
||||
await _hostApi.setZoomLevel(zoom);
|
||||
} on PlatformException catch (e) {
|
||||
throw CameraException(e.code, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setVideoStabilizationMode(int cameraId, VideoStabilizationMode mode) async {
|
||||
try {
|
||||
final Map<VideoStabilizationMode, PlatformVideoStabilizationMode> 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<Iterable<VideoStabilizationMode>> getSupportedVideoStabilizationModes(int cameraId) async {
|
||||
return (await _getSupportedVideoStabilizationModeMap(cameraId)).keys;
|
||||
}
|
||||
|
||||
Future<Map<VideoStabilizationMode, PlatformVideoStabilizationMode>>
|
||||
_getSupportedVideoStabilizationModeMap(int cameraId) async {
|
||||
final ret = <VideoStabilizationMode, PlatformVideoStabilizationMode>{};
|
||||
|
||||
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<void> pausePreview(int cameraId) async {
|
||||
await _hostApi.pausePreview();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> resumePreview(int cameraId) async {
|
||||
await _hostApi.resumePreview();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setDescriptionWhileRecording(CameraDescription description) async {
|
||||
await _hostApi.updateDescriptionWhileRecording(description.name);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setImageFileFormat(int cameraId, ImageFileFormat format) async {
|
||||
await _hostApi.setImageFileFormat(_pigeonImageFileFormat(format));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> 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<double>? 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<DeviceEvent> deviceEventStreamController =
|
||||
StreamController<DeviceEvent>.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<CameraEvent> 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1400
camera_avfoundation/lib/src/messages.g.dart
Normal file
1400
camera_avfoundation/lib/src/messages.g.dart
Normal file
File diff suppressed because it is too large
Load diff
50
camera_avfoundation/lib/src/type_conversion.dart
Normal file
50
camera_avfoundation/lib/src/type_conversion.dart
Normal file
|
|
@ -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<CameraImagePlane>.unmodifiable(
|
||||
data.planes.map<CameraImagePlane>(
|
||||
(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,
|
||||
);
|
||||
}
|
||||
84
camera_avfoundation/lib/src/utils.dart
Normal file
84
camera_avfoundation/lib/src/utils.dart
Normal file
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
3
camera_avfoundation/pigeons/copyright.txt
Normal file
3
camera_avfoundation/pigeons/copyright.txt
Normal file
|
|
@ -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.
|
||||
375
camera_avfoundation/pigeons/messages.dart
Normal file
375
camera_avfoundation/pigeons/messages.dart
Normal file
|
|
@ -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<PlatformCameraImagePlane> 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<PlatformCameraDescription> 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);
|
||||
}
|
||||
37
camera_avfoundation/pubspec.yaml
Normal file
37
camera_avfoundation/pubspec.yaml
Normal file
|
|
@ -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
|
||||
798
camera_avfoundation/test/avfoundation_camera_test.dart
Normal file
798
camera_avfoundation/test/avfoundation_camera_test.dart
Normal file
|
|
@ -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<Object>>[MockSpec<CameraApi>()])
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test('registers instance', () async {
|
||||
AVFoundationCamera.registerWith();
|
||||
expect(CameraPlatform.instance, isA<AVFoundationCamera>());
|
||||
});
|
||||
|
||||
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<CameraException>()
|
||||
.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<CameraException>()
|
||||
.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<void> 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<void> 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<void> 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<CameraInitializedEvent> eventStream = camera.onCameraInitialized(cameraId);
|
||||
final streamQueue = StreamQueue<CameraInitializedEvent>(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<CameraErrorEvent> errorStream = camera.onCameraError(cameraId);
|
||||
final streamQueue = StreamQueue<CameraErrorEvent>(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<DeviceOrientationChangedEvent> eventStream = camera.onDeviceOrientationChanged();
|
||||
final streamQueue = StreamQueue<DeviceOrientationChangedEvent>(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<void> 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>[
|
||||
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<CameraDescription> 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<CameraException>()
|
||||
.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<double>(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<double>(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<CameraException>()
|
||||
.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<VideoStabilizationMode> 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<VideoStabilizationMode> modes = (await camera.getSupportedVideoStabilizationModes(
|
||||
cameraId,
|
||||
)).toList();
|
||||
|
||||
expect(modes, <VideoStabilizationMode>[
|
||||
VideoStabilizationMode.off,
|
||||
VideoStabilizationMode.level1,
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should get all video stabilization modes', () async {
|
||||
when(mockApi.isVideoStabilizationModeSupported(any)).thenAnswer((_) async => true);
|
||||
|
||||
final List<VideoStabilizationMode> modes = (await camera.getSupportedVideoStabilizationModes(
|
||||
cameraId,
|
||||
)).toList();
|
||||
|
||||
expect(modes, <VideoStabilizationMode>[
|
||||
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<ArgumentError>().having((ArgumentError e) => e.name, 'name', 'mode')),
|
||||
);
|
||||
expect(
|
||||
() => camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level1),
|
||||
throwsA(isA<ArgumentError>().having((ArgumentError e) => e.name, 'name', 'mode')),
|
||||
);
|
||||
expect(
|
||||
() => camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level2),
|
||||
throwsA(isA<ArgumentError>().having((ArgumentError e) => e.name, 'name', 'mode')),
|
||||
);
|
||||
expect(
|
||||
() => camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level3),
|
||||
throwsA(isA<ArgumentError>().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<CameraException>()
|
||||
.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<CameraImageData> subscription = camera
|
||||
.onStreamedFrameAvailable(cameraId)
|
||||
.listen((CameraImageData imageData) {});
|
||||
|
||||
verify(mockApi.startImageStream());
|
||||
|
||||
await subscription.cancel();
|
||||
});
|
||||
|
||||
test('Should stop streaming', () async {
|
||||
final StreamSubscription<CameraImageData> 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));
|
||||
});
|
||||
});
|
||||
}
|
||||
354
camera_avfoundation/test/avfoundation_camera_test.mocks.dart
Normal file
354
camera_avfoundation/test/avfoundation_camera_test.mocks.dart
Normal file
|
|
@ -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<String>(
|
||||
this,
|
||||
Invocation.getter(#pigeonVar_messageChannelSuffix),
|
||||
),
|
||||
returnValueForMissingStub: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#pigeonVar_messageChannelSuffix),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
_i4.Future<List<_i2.PlatformCameraDescription>> getAvailableCameras() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getAvailableCameras, []),
|
||||
returnValue: _i4.Future<List<_i2.PlatformCameraDescription>>.value(
|
||||
<_i2.PlatformCameraDescription>[],
|
||||
),
|
||||
returnValueForMissingStub: _i4.Future<List<_i2.PlatformCameraDescription>>.value(
|
||||
<_i2.PlatformCameraDescription>[],
|
||||
),
|
||||
)
|
||||
as _i4.Future<List<_i2.PlatformCameraDescription>>);
|
||||
|
||||
@override
|
||||
_i4.Future<int> create(String? cameraName, _i2.PlatformMediaSettings? settings) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#create, [cameraName, settings]),
|
||||
returnValue: _i4.Future<int>.value(0),
|
||||
returnValueForMissingStub: _i4.Future<int>.value(0),
|
||||
)
|
||||
as _i4.Future<int>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> initialize(int? cameraId, _i2.PlatformImageFormatGroup? imageFormat) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#initialize, [cameraId, imageFormat]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> startImageStream() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#startImageStream, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> stopImageStream() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#stopImageStream, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> receivedImageStreamData() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#receivedImageStreamData, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> dispose(int? cameraId) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#dispose, [cameraId]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> lockCaptureOrientation(_i2.PlatformDeviceOrientation? orientation) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#lockCaptureOrientation, [orientation]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> unlockCaptureOrientation() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#unlockCaptureOrientation, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<String> takePicture() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#takePicture, []),
|
||||
returnValue: _i4.Future<String>.value(
|
||||
_i3.dummyValue<String>(this, Invocation.method(#takePicture, [])),
|
||||
),
|
||||
returnValueForMissingStub: _i4.Future<String>.value(
|
||||
_i3.dummyValue<String>(this, Invocation.method(#takePicture, [])),
|
||||
),
|
||||
)
|
||||
as _i4.Future<String>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> prepareForVideoRecording() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#prepareForVideoRecording, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> startVideoRecording(bool? enableStream) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#startVideoRecording, [enableStream]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<String> stopVideoRecording() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#stopVideoRecording, []),
|
||||
returnValue: _i4.Future<String>.value(
|
||||
_i3.dummyValue<String>(this, Invocation.method(#stopVideoRecording, [])),
|
||||
),
|
||||
returnValueForMissingStub: _i4.Future<String>.value(
|
||||
_i3.dummyValue<String>(this, Invocation.method(#stopVideoRecording, [])),
|
||||
),
|
||||
)
|
||||
as _i4.Future<String>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> pauseVideoRecording() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#pauseVideoRecording, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> resumeVideoRecording() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#resumeVideoRecording, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setFlashMode(_i2.PlatformFlashMode? mode) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setFlashMode, [mode]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setExposureMode(_i2.PlatformExposureMode? mode) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setExposureMode, [mode]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setExposurePoint(_i2.PlatformPoint? point) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setExposurePoint, [point]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<double> getMinExposureOffset() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getMinExposureOffset, []),
|
||||
returnValue: _i4.Future<double>.value(0.0),
|
||||
returnValueForMissingStub: _i4.Future<double>.value(0.0),
|
||||
)
|
||||
as _i4.Future<double>);
|
||||
|
||||
@override
|
||||
_i4.Future<double> getMaxExposureOffset() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getMaxExposureOffset, []),
|
||||
returnValue: _i4.Future<double>.value(0.0),
|
||||
returnValueForMissingStub: _i4.Future<double>.value(0.0),
|
||||
)
|
||||
as _i4.Future<double>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setExposureOffset(double? offset) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setExposureOffset, [offset]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setFocusMode(_i2.PlatformFocusMode? mode) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setFocusMode, [mode]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setFocusPoint(_i2.PlatformPoint? point) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setFocusPoint, [point]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<double> getMinZoomLevel() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getMinZoomLevel, []),
|
||||
returnValue: _i4.Future<double>.value(0.0),
|
||||
returnValueForMissingStub: _i4.Future<double>.value(0.0),
|
||||
)
|
||||
as _i4.Future<double>);
|
||||
|
||||
@override
|
||||
_i4.Future<double> getMaxZoomLevel() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getMaxZoomLevel, []),
|
||||
returnValue: _i4.Future<double>.value(0.0),
|
||||
returnValueForMissingStub: _i4.Future<double>.value(0.0),
|
||||
)
|
||||
as _i4.Future<double>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setZoomLevel(double? zoom) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setZoomLevel, [zoom]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setVideoStabilizationMode(_i2.PlatformVideoStabilizationMode? mode) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setVideoStabilizationMode, [mode]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<bool> isVideoStabilizationModeSupported(_i2.PlatformVideoStabilizationMode? mode) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#isVideoStabilizationModeSupported, [mode]),
|
||||
returnValue: _i4.Future<bool>.value(false),
|
||||
returnValueForMissingStub: _i4.Future<bool>.value(false),
|
||||
)
|
||||
as _i4.Future<bool>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> pausePreview() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#pausePreview, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> resumePreview() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#resumePreview, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> updateDescriptionWhileRecording(String? cameraName) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#updateDescriptionWhileRecording, [cameraName]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setImageFileFormat(_i2.PlatformImageFileFormat? format) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setImageFileFormat, [format]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setJpegImageQuality(int? quality) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setJpegImageQuality, [quality]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
}
|
||||
13
camera_avfoundation/test/flutter_test_config.dart
Normal file
13
camera_avfoundation/test/flutter_test_config.dart
Normal file
|
|
@ -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<void> testExecutable(FutureOr<void> Function() testMain) async {
|
||||
LeakTesting.enable();
|
||||
LeakTracking.warnForUnsupportedPlatforms = false;
|
||||
await testMain();
|
||||
}
|
||||
59
camera_avfoundation/test/type_conversion_test.dart
Normal file
59
camera_avfoundation/test/type_conversion_test.dart
Normal file
|
|
@ -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>[
|
||||
PlatformCameraImagePlane(
|
||||
bytes: Uint8List.fromList(<int>[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>[
|
||||
PlatformCameraImagePlane(
|
||||
bytes: Uint8List.fromList(<int>[1, 2, 3, 4]),
|
||||
bytesPerRow: 4,
|
||||
width: 4,
|
||||
height: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
expect(cameraImage.format.group, ImageFormatGroup.yuv420);
|
||||
});
|
||||
}
|
||||
66
camera_avfoundation/test/utils_test.dart
Normal file
66
camera_avfoundation/test/utils_test.dart
Normal file
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue