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,
|
imageFormatGroup,
|
||||||
);
|
);
|
||||||
imageAnalysis = ImageAnalysis(
|
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,
|
targetFpsRange: _targetFpsRange,
|
||||||
outputImageFormat: _imageAnalysisOutputImageFormat,
|
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