Compare commits

...

9 commits

Author SHA1 Message Date
f5c217c6c1 fix ios audio 2026-10-10 20:14:52 +02:00
adf4fe0ca5 fix performance issues 2026-10-09 17:41:45 +02:00
faaab2bd0d fix issue 2026-10-09 00:28:47 +02:00
95853567e9 add new dep 2026-10-07 22:18:23 +02:00
c3d0858823 fix 2026-10-06 22:13:32 +02:00
8cf0eb53c6 remove emoji_picker as dependencie 2026-10-04 23:03:26 +02:00
4d22ef8490 fix 2026-09-05 01:59:39 +02:00
60da6275f8 add new dep 2026-08-31 22:03:36 +02:00
f46500ce45 remove unused dependencies 2026-08-29 14:59:01 +02:00
473 changed files with 14981 additions and 59912 deletions

View file

@ -1,11 +0,0 @@
Copyright 2021 Philipp Sessler
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. 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.
3. Neither the name of the copyright holder 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 HOLDER 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.

View file

@ -1,6 +0,0 @@
/// Support for doing something awesome.
///
/// More dartdocs go here.
library adaptive_number;
export 'src/number.dart';

View file

@ -1,120 +0,0 @@
import 'package:adaptive_number/src/number.dart';
class NumberInt implements Number {
final int _value;
NumberInt(this._value);
@override
NumberInt operator +(Number value) {
return NumberInt(_value + (value.val as int));
}
@override
NumberInt operator -(Number value) {
return NumberInt(_value - (value.val as int));
}
@override
NumberInt operator -() {
return NumberInt(-(val));
}
@override
NumberInt operator *(Number value) {
return NumberInt(_value * (value.val as int));
}
@override
NumberInt operator &(Number value) {
return NumberInt(_value & (value.val as int));
}
@override
NumberInt operator >>(int value) {
return NumberInt(_value >> value);
}
@override
NumberInt operator <<(int value) {
return NumberInt(_value << value);
}
@override
NumberInt operator ^(Number value) {
return NumberInt(_value ^ (value.val as int));
}
@override
NumberInt operator |(Number value) {
return NumberInt(_value | (value.val as int));
}
@override
bool operator <(Number value) {
return (intValue < value.intValue);
}
@override
bool operator <=(Number value) {
return (intValue <= value.intValue);
}
@override
bool operator >(Number value) {
return (intValue > value.intValue);
}
@override
bool operator >=(Number value) {
return (intValue >= value.intValue);
}
@override
NumberInt operator %(Number value) {
return NumberInt(_value % (value.val as int));
}
@override
Number operator ~/(Number value) {
return NumberInt(_value ~/ (value.val as int));
}
@override
int get val => _value;
@override
int get intValue => _value;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is NumberInt &&
runtimeType == other.runtimeType &&
_value == other._value;
@override
int get hashCode => _value.hashCode;
@override
String toString() {
return _value.toString();
}
@override
String toRadixString(int radix) {
return _value.toRadixString(radix);
}
@override
NumberInt abs() {
return NumberInt(_value.abs());
}
@override
int compareTo(Number other) {
return _value.compareTo(other.intValue);
}
}
Number createNumber(int val) => NumberInt(val);

View file

@ -1,121 +0,0 @@
import 'package:adaptive_number/src/number.dart';
import 'package:fixnum/fixnum.dart';
class NumberInt64 implements Number {
final Int64 _value;
NumberInt64(this._value);
@override
NumberInt64 operator +(Number value) {
return NumberInt64(_value + (value.val as Int64));
}
@override
NumberInt64 operator -(Number value) {
return NumberInt64(_value - (value.val as Int64));
}
@override
NumberInt64 operator -() {
return NumberInt64(-(val));
}
@override
NumberInt64 operator *(Number value) {
return NumberInt64(_value * (value.val as Int64));
}
@override
NumberInt64 operator &(Number value) {
return NumberInt64(_value & (value.val as Int64));
}
@override
NumberInt64 operator >>(int value) {
return NumberInt64(_value >> value);
}
@override
NumberInt64 operator <<(int value) {
return NumberInt64(_value << value);
}
@override
NumberInt64 operator ^(Number value) {
return NumberInt64(_value ^ (value.val as Int64));
}
@override
NumberInt64 operator |(Number value) {
return NumberInt64(_value | (value.val as Int64));
}
@override
bool operator <(Number value) {
return (intValue < value.intValue);
}
@override
bool operator <=(Number value) {
return (intValue <= value.intValue);
}
@override
bool operator >(Number value) {
return (intValue > value.intValue);
}
@override
bool operator >=(Number value) {
return (intValue >= value.intValue);
}
@override
NumberInt64 operator %(Number value) {
return NumberInt64(_value % (value.val as Int64));
}
@override
Number operator ~/(Number value) {
return NumberInt64(_value ~/ (value.val as Int64));
}
@override
Int64 get val => _value;
@override
int get intValue => _value.toInt();
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is NumberInt64 &&
runtimeType == other.runtimeType &&
_value == other._value;
@override
int get hashCode => _value.hashCode;
@override
String toString() {
return _value.toString();
}
@override
String toRadixString(int radix) {
return _value.toRadixString(radix);
}
@override
NumberInt64 abs() {
return NumberInt64(_value.abs());
}
@override
int compareTo(Number value) {
return _value.compareTo(value.val as Int64);
}
}
Number createNumber(int val) => NumberInt64(Int64(val));

View file

@ -1,78 +0,0 @@
import 'package:adaptive_number/src/stub.dart'
// ignore: uri_does_not_exist
if (dart.library.io) 'package:adaptive_number/src/int.dart'
// ignore: uri_does_not_exist
if (dart.library.html) 'package:adaptive_number/src/int64.dart';
abstract class Number {
static Number zero = Number(0);
static Number one = Number(1);
static Number two = Number(2);
factory Number(int val) => createNumber(val);
dynamic get val;
/// Returns the value as int (caution: May overflow on JS runtimes)
int get intValue;
@override
int get hashCode;
/// Addition operator.
Number operator +(Number value);
/// Subtraction operator.
Number operator -(Number value);
/// Negate operator.
Number operator -();
/// Multiplication operator.
Number operator *(Number value);
/// Bitwise and operator.
Number operator &(Number value);
/// Right bit-shift operator.
Number operator >>(int value);
/// Left bit-shift operator.
Number operator <<(int value);
/// Bitwise xor operator.
Number operator ^(Number value);
/// Bitwise or operator.
Number operator |(Number value);
/// Relational less than operator.
bool operator <(Number value);
/// Relational less than or equal to operator.
bool operator <=(Number value);
/// Relational greater than operator.
bool operator >(Number value);
/// Relational greater than or equal to operator.
bool operator >=(Number value);
/// Euclidean modulo operator.
Number operator %(Number value);
/// Truncating division operator.
Number operator ~/(Number value);
@override
String toString();
/// Returns a string representing the value of this integer in the given radix.
String toRadixString(int radix);
/// Returns the absolute value of this integer.
Number abs();
// Compares this to `other`
int compareTo(Number other);
}

View file

@ -1,4 +0,0 @@
import 'package:adaptive_number/src/number.dart';
Number createNumber(int val) => throw UnsupportedError(
'Cannot create a Number without package dart.library.io or dart.library.html being available');

View file

@ -1,17 +0,0 @@
name: adaptive_number
version: 1.0.0
description: >-
Library providing an adaptive number implementation. On JS runtimes, a 64-bit signed fixed-width integer will be used and
for all other platforms the default Dart int data type.
homepage: https://github.com/lemoony/adaptive_number_dart
environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
fixnum: ^1.0.0
dev_dependencies:
pedantic: ^1.10.0
test: ^1.16.0

View file

@ -46,10 +46,13 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion 21
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
} }
dependencies { dependencies {
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'junit:junit:4.13.2'
implementation("androidx.multidex:multidex:2.0.1") implementation("androidx.multidex:multidex:2.0.1")
implementation "com.google.android.exoplayer:exoplayer:2.17.1" implementation "com.google.android.exoplayer:exoplayer:2.17.1"
} }

View file

@ -0,0 +1,51 @@
package com.simform.audio_waveforms.encoders
import android.media.MediaMetadataRetriever
import android.os.SystemClock
import androidx.test.platform.app.InstrumentationRegistry
import com.simform.audio_waveforms.RecorderSettings
import io.flutter.plugin.common.MethodChannel
import org.junit.Assert.*
import org.junit.Test
import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class CommonEncoderTest {
private val result = object : MethodChannel.Result {
override fun success(value: Any?) = Unit
override fun error(code: String, message: String?, details: Any?) {
fail("$code: $message")
}
override fun notImplemented() = fail("Unexpected method")
}
@Test
fun stoppingAfterInputHasDrainedCompletesAndProducesPlayableAudio() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val file = File(context.cacheDir, "encoder-stop-test.m4a")
val encoder = CommonEncoder()
val completed = CountDownLatch(1)
try {
encoder.initCodec(RecorderSettings(path = file.path), result)
// Half a second of mono PCM, followed by an idle microphone. This
// leaves an input buffer held by the encoder when Stop is pressed.
repeat(22) { encoder.queueInputBuffer(ByteArray(2048)) }
SystemClock.sleep(300)
encoder.setOnEncodingCompleted { completed.countDown() }
encoder.signalToStop()
assertTrue("Stopping must return the finished recording", completed.await(5, TimeUnit.SECONDS))
assertTrue(file.length() > 0)
val metadata = MediaMetadataRetriever()
try {
metadata.setDataSource(file.path)
val duration = metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
assertTrue("The recording must retain its audio", duration!!.toLong() >= 400)
} finally {
metadata.release()
}
} finally {
file.delete()
}
}
}

View file

@ -152,22 +152,9 @@ class CommonEncoder {
mediaCodec.setCallback(object : MediaCodec.Callback() { mediaCodec.setCallback(object : MediaCodec.Callback() {
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) { override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
if (isEncodingComplete && inputQueue.isEmpty()) { if (isEncoderStopped) return
// Use the last calculated presentation time for EOF, not system time currentInputBufferIndex = index
val eofTimestamp = if (totalBytesEncoded > 0) { feedEncoder()
val bytesPerSample = 2L
val channels = 1L
(totalBytesEncoded * 1_000_000L) / (recorderSettings.sampleRate * channels * bytesPerSample)
} else {
0L
}
codec.queueInputBuffer(
index, 0, 0, eofTimestamp, MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
} else {
currentInputBufferIndex = index
feedEncoder()
}
} }
override fun onOutputBufferAvailable( override fun onOutputBufferAvailable(
@ -244,7 +231,7 @@ class CommonEncoder {
isMuxerStarted = true isMuxerStarted = true
} }
} }
}) }, handler)
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
mediaCodec.start() mediaCodec.start()
@ -259,11 +246,9 @@ class CommonEncoder {
* @param buffer The raw audio data to encode * @param buffer The raw audio data to encode
*/ */
fun queueInputBuffer(buffer: ByteArray) { fun queueInputBuffer(buffer: ByteArray) {
synchronized(inputQueue) { handler.post {
if (isEncodingComplete || isEncoderStopped) return@post
inputQueue.add(buffer) inputQueue.add(buffer)
}
if (currentInputBufferIndex >= 0) {
feedEncoder() feedEncoder()
} }
} }
@ -275,7 +260,13 @@ class CommonEncoder {
* is processed, the encoder will be sent an end-of-stream signal. * is processed, the encoder will be sent an end-of-stream signal.
*/ */
fun signalToStop() { fun signalToStop() {
isEncodingComplete = true handler.post {
if (isEncoderStopped) return@post
isEncodingComplete = true
// A codec buffer may already be waiting for PCM. No further
// input callback is guaranteed once recording stops.
feedEncoder()
}
} }
/** /**
@ -284,7 +275,7 @@ class CommonEncoder {
* @param callback The function to call when encoding completes * @param callback The function to call when encoding completes
*/ */
fun setOnEncodingCompleted(callback: () -> Unit) { fun setOnEncodingCompleted(callback: () -> Unit) {
completionCallback = callback handler.post { completionCallback = callback }
} }
@ -301,7 +292,18 @@ class CommonEncoder {
*/ */
private fun feedEncoder() { private fun feedEncoder() {
synchronized(inputQueue) { synchronized(inputQueue) {
if (inputQueue.isEmpty() || currentInputBufferIndex < 0) return if (isEncoderStopped || currentInputBufferIndex < 0) return
if (inputQueue.isEmpty()) {
if (isEncodingComplete) {
val timestamp = totalBytesEncoded * 1_000_000L / (recorderSettings.sampleRate * 2L)
mediaCodec.queueInputBuffer(
currentInputBufferIndex, 0, 0, timestamp, MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
currentInputBufferIndex = -1
}
return
}
val data = inputQueue.poll() ?: return val data = inputQueue.poll() ?: return
val inputBuffer = mediaCodec.getInputBuffer(currentInputBufferIndex) ?: return val inputBuffer = mediaCodec.getInputBuffer(currentInputBufferIndex) ?: return
@ -410,7 +412,9 @@ class CommonEncoder {
mediaMuxer?.release() mediaMuxer?.release()
outputStream.close() outputStream.close()
handlerThread.quitSafely() handlerThread.quitSafely()
handlerThread.join() // Codec callbacks run on this thread; joining it here would block
// forever and prevent returning the completed recording to Flutter.
if (Thread.currentThread() !== handlerThread) handlerThread.join()
completionCallback?.invoke() completionCallback?.invoke()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(Constants.LOG_TAG, "Error stopping encoder: ${e.message}") Log.e(Constants.LOG_TAG, "Error stopping encoder: ${e.message}")
@ -426,4 +430,4 @@ class CommonEncoder {
firstOutputTimestamp = -1L firstOutputTimestamp = -1L
lastOutputTimestamp = 0L lastOutputTimestamp = 0L
} }
} }

View file

@ -3,6 +3,8 @@ import "package:avatar_maker/src/core/models/customized_property_category.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_svg/flutter_svg.dart"; import "package:flutter_svg/flutter_svg.dart";
import "package:provider/provider.dart"; import "package:provider/provider.dart";
import "package:vector_graphics/vector_graphics_compat.dart"
show RenderingStrategy;
/// This widget renders the avatar of the user on screen. /// This widget renders the avatar of the user on screen.
/// ///
@ -50,8 +52,10 @@ class AvatarMakerAvatar extends StatelessWidget {
return loader; return loader;
} }
return SvgPicture.string( return SvgPicture.string(
avatarController.drawAvatarSVG(), avatarController.displayedAvatarSVG,
height: radius * 1.6, height: radius * 1.6,
// Reuse the raster while the fixed-size preview scrolls.
renderingStrategy: RenderingStrategy.raster,
semanticsLabel: "Your avatar", semanticsLabel: "Your avatar",
placeholderBuilder: (context) => Center( placeholderBuilder: (context) => Center(
child: loader, child: loader,

View file

@ -94,6 +94,8 @@ class _AvatarMakerCustomizerState extends State<AvatarMakerCustomizer>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late AvatarMakerController avatarMakerController; late AvatarMakerController avatarMakerController;
bool _controllerCreatedInternally = false; bool _controllerCreatedInternally = false;
late final VoidCallback _handleControllerChanged;
late final VoidCallback _handleTabChanged;
/// Number of displayed categories in the customizer widget. /// Number of displayed categories in the customizer widget.
late int nbrDisplayedCategories; late int nbrDisplayedCategories;
@ -121,22 +123,23 @@ class _AvatarMakerCustomizerState extends State<AvatarMakerCustomizer>
vsync: this, vsync: this,
); );
tabController.addListener(() { _handleTabChanged = () {
setState(() {}); if (mounted) setState(() {});
}); };
tabController.addListener(_handleTabChanged);
// Add listener to the controller // Add listener to the controller
avatarMakerController.addListener(() { _handleControllerChanged = () {
setState(() {}); if (mounted) setState(() {});
}); };
avatarMakerController.addListener(_handleControllerChanged);
} }
@override @override
void dispose() { void dispose() {
// Remove the listener avatarMakerController.removeListener(_handleControllerChanged);
avatarMakerController.removeListener(() { tabController.removeListener(_handleTabChanged);
setState(() {}); tabController.dispose();
});
// Only dispose the controller if we created it // Only dispose the controller if we created it
if (_controllerCreatedInternally) { if (_controllerCreatedInternally) {

View file

@ -3,6 +3,8 @@ import "package:avatar_maker/src/customizer/widgets/customizer_appbar.dart";
import "package:avatar_maker/src/customizer/widgets/customizer_bottom_navbar.dart"; import "package:avatar_maker/src/customizer/widgets/customizer_bottom_navbar.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_svg/flutter_svg.dart"; import "package:flutter_svg/flutter_svg.dart";
import "package:vector_graphics/vector_graphics_compat.dart"
show RenderingStrategy;
class CustomizerBody extends StatelessWidget { class CustomizerBody extends StatelessWidget {
final AvatarMakerController avatarMakerController; final AvatarMakerController avatarMakerController;
@ -52,6 +54,8 @@ class CustomizerBody extends StatelessWidget {
/// Build the main Tile Grid with all the options from the attribute /// Build the main Tile Grid with all the options from the attribute
var _tileGrid = GridView.builder( var _tileGrid = GridView.builder(
key: PageStorageKey(propertyCategory.id),
primary: true,
physics: theme.scrollPhysics, physics: theme.scrollPhysics,
itemCount: attributeListLength, itemCount: attributeListLength,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
@ -82,6 +86,8 @@ class CustomizerBody extends StatelessWidget {
avatarMakerController.getComponentSVG( avatarMakerController.getComponentSVG(
propertyCategory.id, index), propertyCategory.id, index),
height: 80, height: 80,
// Avoid replaying SVG paths on every scroll frame.
renderingStrategy: RenderingStrategy.raster,
semanticsLabel: 'Your AvatarMaker', semanticsLabel: 'Your AvatarMaker',
placeholderBuilder: (context) => Center( placeholderBuilder: (context) => Center(
child: CircularProgressIndicator.adaptive(), child: CircularProgressIndicator.adaptive(),

View file

@ -19,6 +19,7 @@ dependencies:
material_symbols_icons: ^4.2874.0 material_symbols_icons: ^4.2874.0
provider: ^6.1.5 provider: ^6.1.5
shared_preferences: ^2.5.3 shared_preferences: ^2.5.3
vector_graphics: ^1.1.19
dev_dependencies: dev_dependencies:
analyzer: ^8.1.1 analyzer: ^8.1.1
build_runner: ^2.10.1 build_runner: ^2.10.1

View file

@ -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,
); );

View 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>

View 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.

View 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.

View 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

View 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.

View 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)
}
}

View 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)
}
}

View file

@ -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))
}
}

View file

@ -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)
}
}

View file

@ -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")
}
}

View 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)
}
}

View 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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View 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))
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View 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)
}
}

View 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)
}
}

View 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."
)
}
}

View 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
}
}
}

View 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)
}
}

View 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) {}
}

View file

@ -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
}
}

View file

@ -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
}
}

View 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
}
}

View file

@ -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) ?? []
}
}

View file

@ -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
}

View 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() {}
}

View file

@ -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)
}
}

View file

@ -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()
}
}

View file

@ -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] = []
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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?
) {}
}

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -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) {}
}

View file

@ -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) {}
}

View file

@ -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
}
}

View file

@ -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(()))
}
}

View file

@ -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)
}
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View file

@ -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)
}
}

View 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)
}
}

View 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

View 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")
]
)
]
)

View file

@ -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 {}

View file

@ -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()
}

View file

@ -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
)
}
}
}

View file

@ -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
}
}

View file

@ -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))
}
}

View file

@ -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")
}
}
}

View file

@ -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(()))
}
}
}

View file

@ -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
}
}

View file

@ -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 {}

View file

@ -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)
}
}

View file

@ -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 }
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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()
}
}
}

View file

@ -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>

View file

@ -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()
}
}
}

View file

@ -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)
}
}

View 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';

View 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,
),
);
}
}

File diff suppressed because it is too large Load diff

View 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,
);
}

View 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,
};
}

View 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.

View 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);
}

View 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

View 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));
});
});
}

View 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>);
}

Some files were not shown because too many files have changed in this diff Show more