Compare commits
9 commits
main
...
rust_rewri
| Author | SHA1 | Date | |
|---|---|---|---|
| f5c217c6c1 | |||
| adf4fe0ca5 | |||
| faaab2bd0d | |||
| 95853567e9 | |||
| c3d0858823 | |||
| 8cf0eb53c6 | |||
| 4d22ef8490 | |||
| 60da6275f8 | |||
| f46500ce45 |
473 changed files with 14981 additions and 59912 deletions
|
|
@ -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.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
/// Support for doing something awesome.
|
||||
///
|
||||
/// More dartdocs go here.
|
||||
library adaptive_number;
|
||||
|
||||
export 'src/number.dart';
|
||||
|
|
@ -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);
|
||||
|
|
@ -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));
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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');
|
||||
|
|
@ -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
|
||||
|
|
@ -46,10 +46,13 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'junit:junit:4.13.2'
|
||||
implementation("androidx.multidex:multidex:2.0.1")
|
||||
implementation "com.google.android.exoplayer:exoplayer:2.17.1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -152,22 +152,9 @@ class CommonEncoder {
|
|||
|
||||
mediaCodec.setCallback(object : MediaCodec.Callback() {
|
||||
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
|
||||
if (isEncodingComplete && inputQueue.isEmpty()) {
|
||||
// Use the last calculated presentation time for EOF, not system time
|
||||
val eofTimestamp = if (totalBytesEncoded > 0) {
|
||||
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()
|
||||
}
|
||||
if (isEncoderStopped) return
|
||||
currentInputBufferIndex = index
|
||||
feedEncoder()
|
||||
}
|
||||
|
||||
override fun onOutputBufferAvailable(
|
||||
|
|
@ -244,7 +231,7 @@ class CommonEncoder {
|
|||
isMuxerStarted = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}, handler)
|
||||
|
||||
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
||||
mediaCodec.start()
|
||||
|
|
@ -259,11 +246,9 @@ class CommonEncoder {
|
|||
* @param buffer The raw audio data to encode
|
||||
*/
|
||||
fun queueInputBuffer(buffer: ByteArray) {
|
||||
synchronized(inputQueue) {
|
||||
handler.post {
|
||||
if (isEncodingComplete || isEncoderStopped) return@post
|
||||
inputQueue.add(buffer)
|
||||
}
|
||||
|
||||
if (currentInputBufferIndex >= 0) {
|
||||
feedEncoder()
|
||||
}
|
||||
}
|
||||
|
|
@ -275,7 +260,13 @@ class CommonEncoder {
|
|||
* is processed, the encoder will be sent an end-of-stream signal.
|
||||
*/
|
||||
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
|
||||
*/
|
||||
fun setOnEncodingCompleted(callback: () -> Unit) {
|
||||
completionCallback = callback
|
||||
handler.post { completionCallback = callback }
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -301,7 +292,18 @@ class CommonEncoder {
|
|||
*/
|
||||
private fun feedEncoder() {
|
||||
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 inputBuffer = mediaCodec.getInputBuffer(currentInputBufferIndex) ?: return
|
||||
|
|
@ -410,7 +412,9 @@ class CommonEncoder {
|
|||
mediaMuxer?.release()
|
||||
outputStream.close()
|
||||
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()
|
||||
} catch (e: Exception) {
|
||||
Log.e(Constants.LOG_TAG, "Error stopping encoder: ${e.message}")
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import "package:avatar_maker/src/core/models/customized_property_category.dart";
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_svg/flutter_svg.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.
|
||||
///
|
||||
|
|
@ -50,8 +52,10 @@ class AvatarMakerAvatar extends StatelessWidget {
|
|||
return loader;
|
||||
}
|
||||
return SvgPicture.string(
|
||||
avatarController.drawAvatarSVG(),
|
||||
avatarController.displayedAvatarSVG,
|
||||
height: radius * 1.6,
|
||||
// Reuse the raster while the fixed-size preview scrolls.
|
||||
renderingStrategy: RenderingStrategy.raster,
|
||||
semanticsLabel: "Your avatar",
|
||||
placeholderBuilder: (context) => Center(
|
||||
child: loader,
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ class _AvatarMakerCustomizerState extends State<AvatarMakerCustomizer>
|
|||
with SingleTickerProviderStateMixin {
|
||||
late AvatarMakerController avatarMakerController;
|
||||
bool _controllerCreatedInternally = false;
|
||||
late final VoidCallback _handleControllerChanged;
|
||||
late final VoidCallback _handleTabChanged;
|
||||
|
||||
/// Number of displayed categories in the customizer widget.
|
||||
late int nbrDisplayedCategories;
|
||||
|
|
@ -121,22 +123,23 @@ class _AvatarMakerCustomizerState extends State<AvatarMakerCustomizer>
|
|||
vsync: this,
|
||||
);
|
||||
|
||||
tabController.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
_handleTabChanged = () {
|
||||
if (mounted) setState(() {});
|
||||
};
|
||||
tabController.addListener(_handleTabChanged);
|
||||
|
||||
// Add listener to the controller
|
||||
avatarMakerController.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
_handleControllerChanged = () {
|
||||
if (mounted) setState(() {});
|
||||
};
|
||||
avatarMakerController.addListener(_handleControllerChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Remove the listener
|
||||
avatarMakerController.removeListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
avatarMakerController.removeListener(_handleControllerChanged);
|
||||
tabController.removeListener(_handleTabChanged);
|
||||
tabController.dispose();
|
||||
|
||||
// Only dispose the controller if we created it
|
||||
if (_controllerCreatedInternally) {
|
||||
|
|
|
|||
|
|
@ -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:flutter/material.dart";
|
||||
import "package:flutter_svg/flutter_svg.dart";
|
||||
import "package:vector_graphics/vector_graphics_compat.dart"
|
||||
show RenderingStrategy;
|
||||
|
||||
class CustomizerBody extends StatelessWidget {
|
||||
final AvatarMakerController avatarMakerController;
|
||||
|
|
@ -52,6 +54,8 @@ class CustomizerBody extends StatelessWidget {
|
|||
|
||||
/// Build the main Tile Grid with all the options from the attribute
|
||||
var _tileGrid = GridView.builder(
|
||||
key: PageStorageKey(propertyCategory.id),
|
||||
primary: true,
|
||||
physics: theme.scrollPhysics,
|
||||
itemCount: attributeListLength,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
|
|
@ -82,6 +86,8 @@ class CustomizerBody extends StatelessWidget {
|
|||
avatarMakerController.getComponentSVG(
|
||||
propertyCategory.id, index),
|
||||
height: 80,
|
||||
// Avoid replaying SVG paths on every scroll frame.
|
||||
renderingStrategy: RenderingStrategy.raster,
|
||||
semanticsLabel: 'Your AvatarMaker',
|
||||
placeholderBuilder: (context) => Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ dependencies:
|
|||
material_symbols_icons: ^4.2874.0
|
||||
provider: ^6.1.5
|
||||
shared_preferences: ^2.5.3
|
||||
vector_graphics: ^1.1.19
|
||||
dev_dependencies:
|
||||
analyzer: ^8.1.1
|
||||
build_runner: ^2.10.1
|
||||
|
|
|
|||
|
|
@ -466,7 +466,20 @@ class AndroidCameraCameraX extends CameraPlatform {
|
|||
imageFormatGroup,
|
||||
);
|
||||
imageAnalysis = ImageAnalysis(
|
||||
resolutionSelector: _presetResolutionSelector,
|
||||
// QR analysis should never copy full-HD frames through the Dart bridge.
|
||||
// Selfie filters use the already reduced 720p preview resolution.
|
||||
resolutionSelector: cameraIsFrontFacing
|
||||
? _presetResolutionSelector
|
||||
: ResolutionSelector(
|
||||
resolutionStrategy: ResolutionStrategy(
|
||||
boundSize: CameraSize(width: 640, height: 360),
|
||||
fallbackRule: ResolutionStrategyFallbackRule.closestLowerThenHigher,
|
||||
),
|
||||
aspectRatioStrategy: AspectRatioStrategy(
|
||||
preferredAspectRatio: AspectRatio.ratio16To9,
|
||||
fallbackRule: AspectRatioStrategyFallbackRule.auto,
|
||||
),
|
||||
),
|
||||
targetFpsRange: _targetFpsRange,
|
||||
outputImageFormat: _imageAnalysisOutputImageFormat,
|
||||
);
|
||||
|
|
|
|||
67
camera_avfoundation/AUTHORS
Normal file
67
camera_avfoundation/AUTHORS
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Below is a list of people and organizations that have contributed
|
||||
# to the Flutter project. Names should be added to the list like so:
|
||||
#
|
||||
# Name/Organization <email address>
|
||||
|
||||
Google Inc.
|
||||
The Chromium Authors
|
||||
German Saprykin <saprykin.h@gmail.com>
|
||||
Benjamin Sauer <sauer.benjamin@gmail.com>
|
||||
larsenthomasj@gmail.com
|
||||
Ali Bitek <alibitek@protonmail.ch>
|
||||
Pol Batlló <pol.batllo@gmail.com>
|
||||
Anatoly Pulyaevskiy
|
||||
Hayden Flinner <haydenflinner@gmail.com>
|
||||
Stefano Rodriguez <hlsroddy@gmail.com>
|
||||
Salvatore Giordano <salvatoregiordanoo@gmail.com>
|
||||
Brian Armstrong <brian@flutter.institute>
|
||||
Paul DeMarco <paulmdemarco@gmail.com>
|
||||
Fabricio Nogueira <feufeu@gmail.com>
|
||||
Simon Lightfoot <simon@devangels.london>
|
||||
Ashton Thomas <ashton@acrinta.com>
|
||||
Thomas Danner <thmsdnnr@gmail.com>
|
||||
Diego Velásquez <diego.velasquez.lopez@gmail.com>
|
||||
Hajime Nakamura <nkmrhj@gmail.com>
|
||||
Tuyển Vũ Xuân <netsoft1985@gmail.com>
|
||||
Miguel Ruivo <miguel@miguelruivo.com>
|
||||
Sarthak Verma <sarthak@artiosys.com>
|
||||
Mike Diarmid <mike@invertase.io>
|
||||
Invertase <oss@invertase.io>
|
||||
Elliot Hesp <elliot@invertase.io>
|
||||
Vince Varga <vince.varga@smaho.com>
|
||||
Aawaz Gyawali <awazgyawali@gmail.com>
|
||||
EUI Limited <ian.evans3@admiralgroup.co.uk>
|
||||
Katarina Sheremet <katarina@sheremet.ch>
|
||||
Thomas Stockx <thomas@stockxit.com>
|
||||
Sarbagya Dhaubanjar <sarbagyastha@gmail.com>
|
||||
Ozkan Eksi <ozeksi@gmail.com>
|
||||
Rishab Nayak <rishab@bu.edu>
|
||||
ko2ic <ko2ic.dev@gmail.com>
|
||||
Jonathan Younger <jonathan@daikini.com>
|
||||
Jose Sanchez <josesm82@gmail.com>
|
||||
Debkanchan Samadder <debu.samadder@gmail.com>
|
||||
Audrius Karosevicius <audrius.karosevicius@gmail.com>
|
||||
Lukasz Piliszczuk <lukasz@intheloup.io>
|
||||
SoundReply Solutions GmbH <ch@soundreply.com>
|
||||
Rafal Wachol <rwachol@gmail.com>
|
||||
Pau Picas <pau.picas@gmail.com>
|
||||
Christian Weder <chrstian.weder@yapeal.ch>
|
||||
Alexandru Tuca <salexandru.tuca@outlook.com>
|
||||
Christian Weder <chrstian.weder@yapeal.ch>
|
||||
Rhodes Davis Jr. <rody.davis.jr@gmail.com>
|
||||
Luigi Agosti <luigi@tengio.com>
|
||||
Quentin Le Guennec <quentin@tengio.com>
|
||||
Koushik Ravikumar <koushik@tengio.com>
|
||||
Nissim Dsilva <nissim@tengio.com>
|
||||
Giancarlo Rocha <giancarloiff@gmail.com>
|
||||
Ryo Miyake <ryo@miyake.id>
|
||||
Théo Champion <contact.theochampion@gmail.com>
|
||||
Kazuki Yamaguchi <y.kazuki0614n@gmail.com>
|
||||
Eitan Schwartz <eshvartz@gmail.com>
|
||||
Chris Rutkowski <chrisrutkowski89@gmail.com>
|
||||
Juan Alvarez <juan.alvarez@resideo.com>
|
||||
Aleksandr Yurkovskiy <sanekyy@gmail.com>
|
||||
Anton Borries <mail@antonborri.es>
|
||||
Alex Li <google@alexv525.com>
|
||||
Rahul Raj <64.rahulraj@gmail.com>
|
||||
Rui Craveiro <ruicraveiro@squarealfa.com>
|
||||
446
camera_avfoundation/CHANGELOG.md
Normal file
446
camera_avfoundation/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,446 @@
|
|||
## 0.10.3
|
||||
|
||||
* Updates pigeon dev_dependency to ^27.3.2 for analyzer 14 compatibility.
|
||||
|
||||
## 0.10.2
|
||||
|
||||
* Adds `setJpegImageQuality` for controlling JPEG compression quality.
|
||||
* Updates minimum supported SDK version to Flutter 3.38/Dart 3.10.
|
||||
|
||||
## 0.10.1
|
||||
|
||||
* Fixes fatal crash on iPhone 17 when using `ResolutionPreset.max`.
|
||||
|
||||
## 0.10.0+3
|
||||
|
||||
* Switches to type-safe Pigeon code for image streaming.
|
||||
|
||||
## 0.10.0+2
|
||||
|
||||
* Updates example to demonstrate correct exception handling for async return statements, ensuring exceptions thrown during return within try blocks are properly caught as per [dart-lang/sdk#44395](https://github.com/dart-lang/sdk/issues/44395).
|
||||
|
||||
## 0.10.0+1
|
||||
|
||||
* Migrates to Swift-based pigeon interface.
|
||||
|
||||
## 0.10.0
|
||||
|
||||
* Adds video stabilization.
|
||||
- Adds getSupportedVideoStabilizationModes method.
|
||||
- Adds setVideoStabilizationMode method.
|
||||
|
||||
## 0.9.23+2
|
||||
|
||||
* Code refactor related to Swift pigeon's generated struct MediaSettings being immutable.
|
||||
|
||||
## 0.9.23+1
|
||||
|
||||
* Fixes error handling in `startWriting`.
|
||||
|
||||
## 0.9.23
|
||||
|
||||
* Changes the default storage path for camera pictures and videos on iOS from the application's Documents directory to the system's temporaryDirectory.
|
||||
|
||||
## 0.9.22+10
|
||||
|
||||
* Handles video and audio interruptions and errors by "pausing" and reporting errors.
|
||||
* Uses a single time offset for both video and audio to avoid desync.
|
||||
|
||||
## 0.9.22+9
|
||||
|
||||
* Migrates `FLTSavePhotoDelegate`, `FLTWritableData`, and `FLTImageStreamHandler` classes to Swift.
|
||||
* Migrates `CameraProperties` and `QueueUtils` helpers to Swift.
|
||||
|
||||
## 0.9.22+8
|
||||
|
||||
* Migrates `FLTPermissionServicing` and `FLTCameraPermissionManager` classes to Swift.
|
||||
|
||||
## 0.9.22+7
|
||||
|
||||
* Migrates `FLTCaptureConnection`, `FLTCaptureDeviceFormat` and `FLTAssetWriter` classes to Swift.
|
||||
|
||||
## 0.9.22+6
|
||||
|
||||
* Updates to Pigeon 26.
|
||||
|
||||
## 0.9.22+5
|
||||
|
||||
* Migrates `FLTCaptureDevice`, `FLTCaptureSession`, and `FLTFormatUtils` classes to Swift.
|
||||
|
||||
## 0.9.22+4
|
||||
|
||||
* Migrates `FLTCameraDeviceDiscovering` and `FLTDeviceOrientationProviding` classes to Swift.
|
||||
|
||||
## 0.9.22+3
|
||||
|
||||
* Updates examples to use the new RadioGroup API instead of deprecated Radio parameters.
|
||||
|
||||
## 0.9.22+2
|
||||
|
||||
* Migrates `FLTCamConfiguration`, `FLTCamMediaSettingsAVWrapper` classes to Swift.
|
||||
* Migrates `FLTCaptureOutput`, `FLTCapturePhotoOutput`, `FLTCaptureVideoDataOutput` protocols to Swift.
|
||||
|
||||
## 0.9.22+1
|
||||
|
||||
* Fixes crash on iOS when `enableAudio` is false.
|
||||
|
||||
## 0.9.22
|
||||
|
||||
* Adds lensType in the PlatformCameraDescription
|
||||
|
||||
## 0.9.21+4
|
||||
|
||||
* Migrates `updateOrientation` and `setCaptureSessionPreset` methods to Swift.
|
||||
* Removes `FLTCam` class.
|
||||
|
||||
## 0.9.21+3
|
||||
|
||||
* Removes code for versions of iOS older than 13.0.
|
||||
* Updates minimum supported SDK version to Flutter 3.35.
|
||||
|
||||
## 0.9.21+2
|
||||
|
||||
* Migrates `DefaultCamera` initialization to Swift.
|
||||
* Removes unused `textureId` field of `FLTCam` class.
|
||||
* Updates minimum supported SDK version to Flutter 3.29/Dart 3.7.
|
||||
|
||||
## 0.9.21+1
|
||||
|
||||
* Migrates `startImageStream` and `setUpCaptureSessionForAudioIfNeeded` methods to Swift.
|
||||
* Removes Objective-C implementation of `reportErrorMessage` method.
|
||||
|
||||
## 0.9.21
|
||||
|
||||
* Fixes crash when streaming is enabled during recording.
|
||||
|
||||
## 0.9.20+7
|
||||
|
||||
* Updates kotlin version to 2.2.0 to enable gradle 8.11 support.
|
||||
|
||||
## 0.9.20+6
|
||||
|
||||
* Migrates `captureToFile` and `getTemporaryFilePath` methods to Swift.
|
||||
* Switches to Swift dispatch queue specific interface.
|
||||
|
||||
## 0.9.20+5
|
||||
|
||||
* Migrates `startVideoRecording`, `setUpVideoRecording`, and `setupWriter` methods to Swift.
|
||||
|
||||
## 0.9.20+4
|
||||
|
||||
* Migrates `setVideoFormat`,`stopVideoRecording`, and `stopImageStream` methods to Swift.
|
||||
* Migrates stopping accelerometer updates to Swift.
|
||||
* Migrates `setDescriptionWhileRecording` method to Swift.
|
||||
* Adds `createConnection` method implementation to Swift.
|
||||
|
||||
## 0.9.20+3
|
||||
|
||||
* Migrates `setZoomLevel` and `setFlashMode` methods to Swift.
|
||||
|
||||
## 0.9.20+2
|
||||
|
||||
* Migrates exposure offset and zoom factor limit getters to Swift.
|
||||
* Migrates `setImageFileFormat` method to Swift.
|
||||
* Migrates pause and resume methods to Swift.
|
||||
* Migrates capture orientation locking methods to Swift.
|
||||
* Converts `setDeviceOrientation` method to property setter and migrated to Swift.
|
||||
|
||||
## 0.9.20+1
|
||||
|
||||
* Migrates lifecycle methods (`start`, `stop`, `close`) to Swift.
|
||||
* Migrates exposure and focus related methods to Swift.
|
||||
* Migrates `receivedImageStreamData` and `reportInitializationState` methods to Swift.
|
||||
|
||||
## 0.9.20
|
||||
|
||||
* Fixes incorrect types in image stream events.
|
||||
|
||||
## 0.9.19+3
|
||||
|
||||
* Fixes race condition when starting image stream.
|
||||
|
||||
## 0.9.19+2
|
||||
|
||||
* Adds the `Camera` Swift protocol.
|
||||
* Adds `DefaultCamera`, a `FLTCam`-based implementation of the `Camera` protocol.
|
||||
* Migrates sample buffer delegates and `FlutterTexture` protocol implementations to `DefaultCamera`.
|
||||
|
||||
## 0.9.19+1
|
||||
|
||||
* Adds `audioCaptureDeviceFactory` to `FLTCamConfiguration`.
|
||||
* Renames the `lockCaptureOrientation` method of Objective-C type `FLTCam` when exported to Swift.
|
||||
* Renames arguments of the `captureOutput` method of Objective-C type `FLTCam` when exported to Swift.
|
||||
* Changes the `connection` argument type of the `captureOutput` method of the of `FLTCam` class to `AVCaptureConnection`.
|
||||
* Makes `minimum/maximumAvailableZoomFactor` and `minimum/maximumExposureOffset` fields of `FLTCam` readonly.
|
||||
* Updates minimum supported SDK version to Flutter 3.27/Dart 3.6.
|
||||
|
||||
## 0.9.19
|
||||
|
||||
* Migrates the CameraPlugin class to Swift.
|
||||
* Fixes camera name being ignored in `setDescriptionWhileRecording`.
|
||||
|
||||
## 0.9.18+14
|
||||
|
||||
* Creates Swift Package Manager target for Swift implementation.
|
||||
|
||||
## 0.9.18+13
|
||||
|
||||
* Migrates test utils and mocks to Swift.
|
||||
* Aligns `FLTCaptureDevice` more closely with the `AVCaptureDevice` interface.
|
||||
|
||||
## 0.9.18+12
|
||||
|
||||
* Migrates test utils and mocks to Swift.
|
||||
* Renames the methods of Objective-C type `FLTCaptureSession` when exported to Swift.
|
||||
|
||||
## 0.9.18+11
|
||||
|
||||
* Backfills unit tests for the `FLTCam` class.
|
||||
* Refactors implementation to allow mocking of `AVCaptureVideoDataOutput` in tests.
|
||||
|
||||
## 0.9.18+10
|
||||
|
||||
* Backfills unit tests for the `FLTCam` class.
|
||||
* Makes `exposureMode`, `focusMode`, and `flashMode` properties of `FLTCam` private.
|
||||
|
||||
## 0.9.18+9
|
||||
|
||||
* Backfills unit tests for `CameraPlugin` class.
|
||||
* Adds `minimumExposureOffset` and `maximumExposureOffset` methods to `FLTCam` class.
|
||||
|
||||
## 0.9.18+8
|
||||
|
||||
* Migrates unit tests to Swift.
|
||||
|
||||
## 0.9.18+7
|
||||
|
||||
* Fixes crash when setting `activeFormat` on `FLTCaptureDevice`.
|
||||
|
||||
## 0.9.18+6
|
||||
|
||||
* Refactors implementations to remove remaining usage of OCMock in internal testing.
|
||||
|
||||
## 0.9.18+5
|
||||
|
||||
* Migrates unit tests to Swift.
|
||||
|
||||
## 0.9.18+4
|
||||
|
||||
* Refactors implementations to reduce usage of OCMock in internal testing.
|
||||
|
||||
## 0.9.18+3
|
||||
|
||||
* Refactors implementations to reduce usage of OCMock in internal testing.
|
||||
|
||||
## 0.9.18+2
|
||||
|
||||
* Refactors implementations to reduce usage of OCMock in internal testing.
|
||||
|
||||
## 0.9.18+1
|
||||
|
||||
* Refactors implementations to reduce usage of OCMock in internal testing.
|
||||
|
||||
## 0.9.18
|
||||
|
||||
* Adds API support query for image streaming.
|
||||
|
||||
## 0.9.17+7
|
||||
|
||||
* Fixes changing global audio session category to be collision free across plugins.
|
||||
|
||||
## 0.9.17+6
|
||||
|
||||
* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4.
|
||||
* Removes OCMock usage from permission tests
|
||||
|
||||
## 0.9.17+5
|
||||
|
||||
* Adds ability to use any supported FPS and fixes crash when using unsupported FPS.
|
||||
|
||||
## 0.9.17+4
|
||||
|
||||
* Updates Pigeon for non-nullable collection type support.
|
||||
* Updates minimum supported SDK version to Flutter 3.19/Dart 3.3.
|
||||
|
||||
## 0.9.17+3
|
||||
|
||||
* Fixes deallocation of camera on dispose.
|
||||
|
||||
## 0.9.17+2
|
||||
|
||||
* Fixes stopVideoRecording waiting indefinitely and lag at start of video.
|
||||
|
||||
## 0.9.17+1
|
||||
|
||||
* Fixes a crash due to appending sample buffers when readyForMoreMediaData is NO.
|
||||
|
||||
## 0.9.17
|
||||
|
||||
* Adds Swift Package Manager compatibility.
|
||||
|
||||
## 0.9.16+3
|
||||
|
||||
* Removes unused `maxVideoDuration` code.
|
||||
|
||||
## 0.9.16+2
|
||||
|
||||
* Fixes regression taking a picture in torch mode.
|
||||
|
||||
## 0.9.16+1
|
||||
|
||||
* Fixes sample times not being numeric after pause/resume.
|
||||
|
||||
## 0.9.16
|
||||
|
||||
* Converts Dart-to-host communcation to Pigeon.
|
||||
* Fixes a race condition in camera disposal.
|
||||
|
||||
## 0.9.15+4
|
||||
|
||||
* Converts host-to-Dart communcation to Pigeon.
|
||||
|
||||
## 0.9.15+3
|
||||
|
||||
* Moves `pigeon` to `dev_dependencies`.
|
||||
|
||||
## 0.9.15+2
|
||||
|
||||
* Converts camera query to Pigeon.
|
||||
|
||||
## 0.9.15+1
|
||||
|
||||
* Simplifies internal handling of method channel responses.
|
||||
|
||||
## 0.9.15
|
||||
|
||||
* Adds support to control video FPS and bitrate. See `CameraController.withSettings`.
|
||||
|
||||
## 0.9.14+2
|
||||
|
||||
* Removes `_ambiguate` methods from example code.
|
||||
|
||||
## 0.9.14+1
|
||||
|
||||
* Fixes bug where max resolution preset does not produce highest available resolution on iOS.
|
||||
|
||||
## 0.9.14
|
||||
|
||||
* Adds support to HEIF format.
|
||||
|
||||
## 0.9.13+11
|
||||
|
||||
* Fixes a memory leak of sample buffer when pause and resume the video recording.
|
||||
* Removes development team from example app.
|
||||
* Updates minimum iOS version to 12.0 and minimum Flutter version to 3.16.6.
|
||||
|
||||
## 0.9.13+10
|
||||
|
||||
* Adds privacy manifest.
|
||||
|
||||
## 0.9.13+9
|
||||
|
||||
* Fixes new lint warnings.
|
||||
|
||||
## 0.9.13+8
|
||||
|
||||
* Updates example app to use non-deprecated video_player method.
|
||||
* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0.
|
||||
|
||||
## 0.9.13+7
|
||||
|
||||
* Fixes inverted orientation strings.
|
||||
|
||||
## 0.9.13+6
|
||||
|
||||
* Fixes incorrect use of `NSError` that could cause crashes on launch.
|
||||
|
||||
## 0.9.13+5
|
||||
|
||||
* Ignores audio samples until the first video sample arrives.
|
||||
|
||||
## 0.9.13+4
|
||||
|
||||
* Adds pub topics to package metadata.
|
||||
* Updates minimum supported SDK version to Flutter 3.7/Dart 2.19.
|
||||
|
||||
## 0.9.13+3
|
||||
|
||||
* Migrates `styleFrom` usage in examples off of deprecated `primary` and `onPrimary` parameters.
|
||||
* Fixes unawaited_futures violations.
|
||||
|
||||
## 0.9.13+2
|
||||
|
||||
* Removes obsolete null checks on non-nullable values.
|
||||
* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18.
|
||||
|
||||
## 0.9.13+1
|
||||
|
||||
* Clarifies explanation of endorsement in README.
|
||||
|
||||
## 0.9.13
|
||||
|
||||
* Allows camera to be switched while video recording.
|
||||
* Aligns Dart and Flutter SDK constraints.
|
||||
|
||||
## 0.9.12
|
||||
|
||||
* Updates minimum Flutter version to 3.3 and iOS 11.
|
||||
|
||||
## 0.9.11+1
|
||||
|
||||
* Updates links for the merge of flutter/plugins into flutter/packages.
|
||||
|
||||
## 0.9.11
|
||||
|
||||
* Adds back use of Optional type.
|
||||
* Updates minimum Flutter version to 3.0.
|
||||
|
||||
## 0.9.10+2
|
||||
|
||||
* Updates code for stricter lint checks.
|
||||
|
||||
## 0.9.10+1
|
||||
|
||||
* Updates code for stricter lint checks.
|
||||
|
||||
## 0.9.10
|
||||
|
||||
* Remove usage of deprecated quiver Optional type.
|
||||
|
||||
## 0.9.9
|
||||
|
||||
* Implements option to also stream when recording a video.
|
||||
|
||||
## 0.9.8+6
|
||||
|
||||
* Updates code for `no_leading_underscores_for_local_identifiers` lint.
|
||||
* Updates minimum Flutter version to 2.10.
|
||||
|
||||
## 0.9.8+5
|
||||
|
||||
* Fixes a regression introduced in 0.9.8+4 where the stream handler is not set.
|
||||
|
||||
## 0.9.8+4
|
||||
|
||||
* Fixes a crash due to sending orientation change events when the engine is torn down.
|
||||
|
||||
## 0.9.8+3
|
||||
|
||||
* Fixes avoid_redundant_argument_values lint warnings and minor typos.
|
||||
* Ignores missing return warnings in preparation for [upcoming analysis changes](https://github.com/flutter/flutter/issues/105750).
|
||||
|
||||
## 0.9.8+2
|
||||
|
||||
* Fixes exception in registerWith caused by the switch to an in-package method channel.
|
||||
|
||||
## 0.9.8+1
|
||||
|
||||
* Ignores deprecation warnings for upcoming styleFrom button API changes.
|
||||
|
||||
## 0.9.8
|
||||
|
||||
* Switches to internal method channel implementation.
|
||||
|
||||
## 0.9.7+1
|
||||
|
||||
* Splits from `camera` as a federated implementation.
|
||||
25
camera_avfoundation/LICENSE
Normal file
25
camera_avfoundation/LICENSE
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
Copyright 2013 The Flutter Authors
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
15
camera_avfoundation/README.md
Normal file
15
camera_avfoundation/README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# camera\_avfoundation
|
||||
|
||||
The iOS implementation of [`camera`][1].
|
||||
|
||||
## Usage
|
||||
|
||||
This package is [endorsed][2], which means you can simply use `camera`
|
||||
normally. This package will be automatically included in your app when you do,
|
||||
so you do not need to add it to your `pubspec.yaml`.
|
||||
|
||||
However, if you `import` this package to use any of its APIs directly, you
|
||||
should add it to your `pubspec.yaml` as usual.
|
||||
|
||||
[1]: https://pub.dev/packages/camera
|
||||
[2]: https://flutter.dev/to/endorsed-federated-plugin
|
||||
11
camera_avfoundation/TWONLY_CHANGES.md
Normal file
11
camera_avfoundation/TWONLY_CHANGES.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
Based on Flutter's `camera_avfoundation` 0.10.3 (BSD license).
|
||||
|
||||
Local changes:
|
||||
|
||||
- Downscale rear-camera BGRA analysis frames to a maximum edge of 640 pixels
|
||||
with Accelerate, before copying them to Dart. Limit delivery to 10 fps.
|
||||
Preview textures, video recording, and front-camera frame delivery are unchanged.
|
||||
- Cover stream resizing with `CameraImageStreamScalerTests`.
|
||||
|
||||
The app configures front-camera sessions at 720p and rear-camera sessions at
|
||||
1080p. The shutter uses the preview screenshot path.
|
||||
138
camera_avfoundation/darwin/Tests/AvailableCamerasTests.swift
Normal file
138
camera_avfoundation/darwin/Tests/AvailableCamerasTests.swift
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class AvailableCamerasTest: XCTestCase {
|
||||
private func createCameraPlugin(with deviceDiscoverer: MockCameraDeviceDiscoverer) -> CameraPlugin
|
||||
{
|
||||
return CameraPlugin(
|
||||
registry: MockFlutterTextureRegistry(),
|
||||
messenger: MockFlutterBinaryMessenger(),
|
||||
globalAPI: MockGlobalEventApi(),
|
||||
deviceDiscoverer: deviceDiscoverer,
|
||||
permissionManager: MockCameraPermissionManager(),
|
||||
deviceFactory: { _ in MockCaptureDevice() },
|
||||
captureSessionFactory: { MockCaptureSession() },
|
||||
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
|
||||
captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue")
|
||||
)
|
||||
}
|
||||
|
||||
func testAvailableCamerasShouldReturnAllCamerasOnMultiCameraIPhone() {
|
||||
let mockDeviceDiscoverer = MockCameraDeviceDiscoverer()
|
||||
let cameraPlugin = createCameraPlugin(with: mockDeviceDiscoverer)
|
||||
let expectation = self.expectation(description: "Result finished")
|
||||
|
||||
mockDeviceDiscoverer.discoverySessionStub = { deviceTypes, mediaType, position in
|
||||
// iPhone 13 Cameras:
|
||||
let wideAngleCamera = MockCaptureDevice()
|
||||
wideAngleCamera.uniqueID = "0"
|
||||
wideAngleCamera.position = .back
|
||||
|
||||
let frontFacingCamera = MockCaptureDevice()
|
||||
frontFacingCamera.uniqueID = "1"
|
||||
frontFacingCamera.position = .front
|
||||
|
||||
let ultraWideCamera = MockCaptureDevice()
|
||||
ultraWideCamera.uniqueID = "2"
|
||||
ultraWideCamera.position = .back
|
||||
|
||||
let telephotoCamera = MockCaptureDevice()
|
||||
telephotoCamera.uniqueID = "3"
|
||||
telephotoCamera.position = .back
|
||||
|
||||
var requiredTypes: [AVCaptureDevice.DeviceType] = [
|
||||
.builtInWideAngleCamera, .builtInTelephotoCamera, .builtInUltraWideCamera,
|
||||
]
|
||||
var cameras = [wideAngleCamera, frontFacingCamera, telephotoCamera, ultraWideCamera]
|
||||
|
||||
XCTAssertEqual(deviceTypes, requiredTypes)
|
||||
XCTAssertEqual(mediaType, .video)
|
||||
XCTAssertEqual(position, .unspecified)
|
||||
return cameras
|
||||
}
|
||||
|
||||
var resultValue: [PlatformCameraDescription]?
|
||||
cameraPlugin.getAvailableCameras { result in
|
||||
resultValue = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
// Verify the result.
|
||||
XCTAssertEqual(resultValue?.count, 4)
|
||||
}
|
||||
|
||||
func testAvailableCamerasShouldReturnTwoCamerasOnDualCameraIPhone() {
|
||||
let mockDeviceDiscoverer = MockCameraDeviceDiscoverer()
|
||||
let cameraPlugin = createCameraPlugin(with: mockDeviceDiscoverer)
|
||||
let expectation = self.expectation(description: "Result finished")
|
||||
|
||||
mockDeviceDiscoverer.discoverySessionStub = { deviceTypes, mediaType, position in
|
||||
// iPhone 8 Cameras:
|
||||
let wideAngleCamera = MockCaptureDevice()
|
||||
wideAngleCamera.uniqueID = "0"
|
||||
wideAngleCamera.position = .back
|
||||
|
||||
let frontFacingCamera = MockCaptureDevice()
|
||||
frontFacingCamera.uniqueID = "1"
|
||||
frontFacingCamera.position = .front
|
||||
|
||||
var requiredTypes: [AVCaptureDevice.DeviceType] = [
|
||||
.builtInWideAngleCamera, .builtInTelephotoCamera, .builtInUltraWideCamera,
|
||||
]
|
||||
let cameras = [wideAngleCamera, frontFacingCamera]
|
||||
|
||||
XCTAssertEqual(deviceTypes, requiredTypes)
|
||||
XCTAssertEqual(mediaType, .video)
|
||||
XCTAssertEqual(position, .unspecified)
|
||||
return cameras
|
||||
}
|
||||
|
||||
var resultValue: [PlatformCameraDescription]?
|
||||
cameraPlugin.getAvailableCameras { result in
|
||||
resultValue = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
// Verify the result.
|
||||
XCTAssertEqual(resultValue?.count, 2)
|
||||
}
|
||||
|
||||
func testAvailableCamerasShouldReturnExternalLensDirectionForUnspecifiedCameraPosition() {
|
||||
let mockDeviceDiscoverer = MockCameraDeviceDiscoverer()
|
||||
let cameraPlugin = createCameraPlugin(with: mockDeviceDiscoverer)
|
||||
let expectation = self.expectation(description: "Result finished")
|
||||
|
||||
mockDeviceDiscoverer.discoverySessionStub = { deviceTypes, mediaType, position in
|
||||
let unspecifiedCamera = MockCaptureDevice()
|
||||
unspecifiedCamera.uniqueID = "0"
|
||||
unspecifiedCamera.position = .unspecified
|
||||
|
||||
var requiredTypes: [AVCaptureDevice.DeviceType] = [
|
||||
.builtInWideAngleCamera, .builtInTelephotoCamera, .builtInUltraWideCamera,
|
||||
]
|
||||
let cameras = [unspecifiedCamera]
|
||||
|
||||
XCTAssertEqual(deviceTypes, requiredTypes)
|
||||
XCTAssertEqual(mediaType, .video)
|
||||
XCTAssertEqual(position, .unspecified)
|
||||
return cameras
|
||||
}
|
||||
|
||||
var resultValue: [PlatformCameraDescription]?
|
||||
cameraPlugin.getAvailableCameras { result in
|
||||
resultValue = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertEqual(resultValue?.first?.lensDirection, .external)
|
||||
}
|
||||
}
|
||||
146
camera_avfoundation/darwin/Tests/CameraExposureTests.swift
Normal file
146
camera_avfoundation/darwin/Tests/CameraExposureTests.swift
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class CameraExposureTests: XCTestCase {
|
||||
private func createCamera() -> (Camera, MockCaptureDevice, MockDeviceOrientationProvider) {
|
||||
let mockDevice = MockCaptureDevice()
|
||||
let mockDeviceOrientationProvider = MockDeviceOrientationProvider()
|
||||
|
||||
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
||||
configuration.videoCaptureDeviceFactory = { _ in mockDevice }
|
||||
configuration.deviceOrientationProvider = mockDeviceOrientationProvider
|
||||
let camera = CameraTestUtils.createTestCamera(configuration)
|
||||
|
||||
return (camera, mockDevice, mockDeviceOrientationProvider)
|
||||
}
|
||||
|
||||
func testSetExposureModeLocked_setsAuthExposeMode() {
|
||||
let (camera, mockDevice, _) = createCamera()
|
||||
|
||||
mockDevice.setExposureModeStub = { mode in
|
||||
// AVCaptureExposureModeAutoExpose automatically adjusts the exposure one time, and then
|
||||
// locks exposure for the device
|
||||
XCTAssertEqual(mode, .autoExpose)
|
||||
}
|
||||
|
||||
camera.setExposureMode(.locked)
|
||||
}
|
||||
|
||||
func testSetExposureModeAuto_setsContinousAutoExposureMode_ifSupported() {
|
||||
let (camera, mockDevice, _) = createCamera()
|
||||
|
||||
// All exposure modes are supported
|
||||
mockDevice.isExposureModeSupportedStub = { _ in true }
|
||||
|
||||
mockDevice.setExposureModeStub = { mode in
|
||||
XCTAssertEqual(mode, .continuousAutoExposure)
|
||||
}
|
||||
|
||||
camera.setExposureMode(.auto)
|
||||
}
|
||||
|
||||
func testSetExposureModeAuto_setsAutoExposeMode_ifContinousAutoIsNotSupported() {
|
||||
let (camera, mockDevice, _) = createCamera()
|
||||
|
||||
// Continous auto exposure is not supported
|
||||
mockDevice.isExposureModeSupportedStub = { mode in
|
||||
mode != .continuousAutoExposure
|
||||
}
|
||||
|
||||
mockDevice.setExposureModeStub = { mode in
|
||||
XCTAssertEqual(mode, .autoExpose)
|
||||
}
|
||||
|
||||
camera.setExposureMode(.auto)
|
||||
}
|
||||
|
||||
func testSetExposurePoint_setsExposurePointOfInterest() {
|
||||
let (camera, mockDevice, mockDeviceOrientationProvider) = createCamera()
|
||||
// UI is currently in landscape left orientation.
|
||||
mockDeviceOrientationProvider.orientationStub = { .landscapeLeft }
|
||||
// Exposure point of interest is supported.
|
||||
mockDevice.isExposurePointOfInterestSupported = true
|
||||
|
||||
// Verify the focus point of interest has been set.
|
||||
var setPoint = CGPoint.zero
|
||||
mockDevice.setExposurePointOfInterestStub = { point in
|
||||
if point == CGPoint(x: 1, y: 1) {
|
||||
setPoint = point
|
||||
}
|
||||
}
|
||||
|
||||
let expectation = expectation(description: "Completion called")
|
||||
camera.setExposurePoint(PlatformPoint(x: 1, y: 1)) {
|
||||
result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
XCTAssertEqual(setPoint, CGPoint(x: 1.0, y: 1.0))
|
||||
}
|
||||
|
||||
func testSetExposurePoint_returnsError_ifNotSupported() {
|
||||
let (camera, mockDevice, mockDeviceOrientationProvider) = createCamera()
|
||||
// UI is currently in landscape left orientation.
|
||||
mockDeviceOrientationProvider.orientationStub = { .landscapeLeft }
|
||||
// Exposure point of interest is not supported.
|
||||
mockDevice.isExposurePointOfInterestSupported = false
|
||||
|
||||
let expectation = expectation(description: "Completion with error")
|
||||
|
||||
camera.setExposurePoint(PlatformPoint(x: 1, y: 1)) { result in
|
||||
switch result {
|
||||
case .failure(let error as PigeonError):
|
||||
XCTAssertEqual(error.code, "setExposurePointFailed")
|
||||
XCTAssertEqual(error.message, "Device does not have exposure point capabilities")
|
||||
default:
|
||||
XCTFail("Expected failure")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testSetExposureOffset_setsExposureTargetBias() {
|
||||
let (camera, mockDevice, _) = createCamera()
|
||||
|
||||
let targetOffset = CGFloat(1.0)
|
||||
|
||||
var setExposureTargetBiasCalled = false
|
||||
mockDevice.setExposureTargetBiasStub = { bias, handler in
|
||||
XCTAssertEqual(bias, Float(targetOffset))
|
||||
setExposureTargetBiasCalled = true
|
||||
}
|
||||
|
||||
camera.setExposureOffset(targetOffset)
|
||||
|
||||
XCTAssertTrue(setExposureTargetBiasCalled)
|
||||
}
|
||||
|
||||
func testMaximumExposureOffset_returnsDeviceMaxExposureTargetBias() {
|
||||
let (camera, mockDevice, _) = createCamera()
|
||||
|
||||
let targetOffset = CGFloat(1.0)
|
||||
|
||||
mockDevice.maxExposureTargetBias = Float(targetOffset)
|
||||
|
||||
XCTAssertEqual(camera.maximumExposureOffset, targetOffset)
|
||||
}
|
||||
|
||||
func testMinimumExposureOffset_returnsDeviceMinExposureTargetBias() {
|
||||
let (camera, mockDevice, _) = createCamera()
|
||||
|
||||
let targetOffset = CGFloat(1.0)
|
||||
|
||||
mockDevice.minExposureTargetBias = Float(targetOffset)
|
||||
|
||||
XCTAssertEqual(camera.minimumExposureOffset, targetOffset)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright 2026 twonly contributors. BSD-style license; see LICENSE.
|
||||
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class CameraImageStreamScalerTests: XCTestCase {
|
||||
func testReducesBGRAFrameWithoutChangingPreviewBuffer() throws {
|
||||
var pixelBuffer: CVPixelBuffer?
|
||||
XCTAssertEqual(
|
||||
CVPixelBufferCreate(
|
||||
kCFAllocatorDefault, 1920, 1080, kCVPixelFormatType_32BGRA, nil, &pixelBuffer),
|
||||
kCVReturnSuccess)
|
||||
let buffer = try XCTUnwrap(pixelBuffer)
|
||||
CVPixelBufferLockBaseAddress(buffer, [])
|
||||
defer { CVPixelBufferUnlockBaseAddress(buffer, []) }
|
||||
let address = try XCTUnwrap(CVPixelBufferGetBaseAddress(buffer))
|
||||
memset(address, 127, CVPixelBufferGetBytesPerRow(buffer) * 1080)
|
||||
|
||||
let plane = try XCTUnwrap(CameraImageStreamScaler.scaleBGRA(buffer, maximumDimension: 640))
|
||||
XCTAssertEqual(plane.width, 640)
|
||||
XCTAssertEqual(plane.height, 360)
|
||||
XCTAssertEqual(plane.bytesPerRow, 2560)
|
||||
XCTAssertEqual(plane.bytes.data.count, 640 * 360 * 4)
|
||||
XCTAssertEqual(plane.bytes.data.first, 127)
|
||||
XCTAssertEqual(CVPixelBufferGetWidth(buffer), 1920)
|
||||
XCTAssertEqual(CVPixelBufferGetHeight(buffer), 1080)
|
||||
XCTAssertEqual(address.load(as: UInt8.self), 127)
|
||||
}
|
||||
|
||||
func testDoesNotUpscaleSmallFrames() throws {
|
||||
var pixelBuffer: CVPixelBuffer?
|
||||
XCTAssertEqual(
|
||||
CVPixelBufferCreate(
|
||||
kCFAllocatorDefault, 320, 240, kCVPixelFormatType_32BGRA, nil, &pixelBuffer),
|
||||
kCVReturnSuccess)
|
||||
let buffer = try XCTUnwrap(pixelBuffer)
|
||||
CVPixelBufferLockBaseAddress(buffer, [])
|
||||
defer { CVPixelBufferUnlockBaseAddress(buffer, []) }
|
||||
XCTAssertNil(CameraImageStreamScaler.scaleBGRA(buffer, maximumDimension: 640))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class CameraInitRaceConditionsTests: XCTestCase {
|
||||
private func createCameraPlugin() -> (CameraPlugin, DispatchQueue) {
|
||||
let captureSessionQueue = DispatchQueue(label: "io.flutter.camera.captureSessionQueue")
|
||||
|
||||
let cameraPlugin = CameraPlugin(
|
||||
registry: MockFlutterTextureRegistry(),
|
||||
messenger: MockFlutterBinaryMessenger(),
|
||||
globalAPI: MockGlobalEventApi(),
|
||||
deviceDiscoverer: MockCameraDeviceDiscoverer(),
|
||||
permissionManager: MockCameraPermissionManager(),
|
||||
deviceFactory: { _ in MockCaptureDevice() },
|
||||
captureSessionFactory: { MockCaptureSession() },
|
||||
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
|
||||
captureSessionQueue: captureSessionQueue
|
||||
)
|
||||
|
||||
return (cameraPlugin, captureSessionQueue)
|
||||
}
|
||||
|
||||
func testFixForCaptureSessionQueueNullPointerCrashDueToRaceCondition() {
|
||||
let (cameraPlugin, captureSessionQueue) = createCameraPlugin()
|
||||
let disposeExpectation = expectation(description: "dispose's result block must be called")
|
||||
let createExpectation = expectation(description: "create's result block must be called")
|
||||
|
||||
// Mimic a dispose call followed by a create call, which can be triggered by slightly dragging the
|
||||
// home bar, causing the app to be inactive, and immediately regain active.
|
||||
cameraPlugin.dispose(cameraId: 0) { error in
|
||||
disposeExpectation.fulfill()
|
||||
}
|
||||
|
||||
cameraPlugin.createCameraOnSessionQueue(
|
||||
withName: "acamera",
|
||||
settings: PlatformMediaSettings(
|
||||
resolutionPreset: .medium,
|
||||
framesPerSecond: nil,
|
||||
videoBitrate: nil,
|
||||
audioBitrate: nil,
|
||||
enableAudio: true
|
||||
)
|
||||
) { result in
|
||||
createExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
// `captureSessionQueue` must not be nil after `create` call. Otherwise a nil
|
||||
// `captureSessionQueue` passed into `AVCaptureVideoDataOutput::setSampleBufferDelegate:queue:`
|
||||
// API will cause a crash.
|
||||
XCTAssertNotNil(
|
||||
captureSessionQueue, "captureSessionQueue must not be nil after create method.")
|
||||
}
|
||||
|
||||
func testFlutterChannelInitializedWhenStartingImageStream() {
|
||||
let (cameraPlugin, _captureSessionQueue) = createCameraPlugin()
|
||||
let createExpectation = expectation(description: "create's result block must be called")
|
||||
|
||||
cameraPlugin.createCameraOnSessionQueue(
|
||||
withName: "acamera",
|
||||
settings: PlatformMediaSettings(
|
||||
resolutionPreset: .medium,
|
||||
framesPerSecond: nil,
|
||||
videoBitrate: nil,
|
||||
audioBitrate: nil,
|
||||
enableAudio: true
|
||||
)
|
||||
) { result in
|
||||
createExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
// Start stream and wait for its completion.
|
||||
let startStreamExpectation = expectation(
|
||||
description: "startImageStream's result block must be called")
|
||||
cameraPlugin.startImageStream(completion: {
|
||||
_ in
|
||||
startStreamExpectation.fulfill()
|
||||
})
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
XCTAssertEqual(cameraPlugin.camera?.isStreamingImages, true)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class CameraMethodChannelTests: XCTestCase {
|
||||
private func createCameraPlugin(with session: MockCaptureSession) -> CameraPlugin {
|
||||
return CameraPlugin(
|
||||
registry: MockFlutterTextureRegistry(),
|
||||
messenger: MockFlutterBinaryMessenger(),
|
||||
globalAPI: MockGlobalEventApi(),
|
||||
deviceDiscoverer: MockCameraDeviceDiscoverer(),
|
||||
permissionManager: MockCameraPermissionManager(),
|
||||
deviceFactory: { _ in MockCaptureDevice() },
|
||||
captureSessionFactory: { session },
|
||||
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
|
||||
captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue")
|
||||
)
|
||||
}
|
||||
|
||||
func testCreate_ShouldCallResultOnMainThread() {
|
||||
let avCaptureSessionMock = MockCaptureSession()
|
||||
avCaptureSessionMock.canSetSessionPresetStub = { _ in true }
|
||||
let camera = createCameraPlugin(with: avCaptureSessionMock)
|
||||
let expectation = self.expectation(description: "Result finished")
|
||||
|
||||
var resultValue: Int64?
|
||||
camera.createCameraOnSessionQueue(
|
||||
withName: "acamera",
|
||||
settings: PlatformMediaSettings(
|
||||
resolutionPreset: .medium,
|
||||
framesPerSecond: nil,
|
||||
videoBitrate: nil,
|
||||
audioBitrate: nil,
|
||||
enableAudio: true
|
||||
)
|
||||
) { result in
|
||||
resultValue = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
XCTAssertNotNil(resultValue)
|
||||
}
|
||||
|
||||
func testDisposeShouldDeallocCamera() {
|
||||
let avCaptureSessionMock = MockCaptureSession()
|
||||
avCaptureSessionMock.canSetSessionPresetStub = { _ in true }
|
||||
let camera = createCameraPlugin(with: avCaptureSessionMock)
|
||||
let createExpectation = self.expectation(description: "create's result block must be called")
|
||||
|
||||
camera.createCameraOnSessionQueue(
|
||||
withName: "acamera",
|
||||
settings: PlatformMediaSettings(
|
||||
resolutionPreset: .medium,
|
||||
framesPerSecond: nil,
|
||||
videoBitrate: nil,
|
||||
audioBitrate: nil,
|
||||
enableAudio: true
|
||||
)
|
||||
) { result in
|
||||
createExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
XCTAssertNotNil(camera.camera)
|
||||
|
||||
let disposeExpectation = self.expectation(description: "dispose's result block must be called")
|
||||
camera.dispose(cameraId: 0) { error in
|
||||
disposeExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
XCTAssertNil(camera.camera, "camera should be deallocated after dispose")
|
||||
}
|
||||
}
|
||||
168
camera_avfoundation/darwin/Tests/CameraOrientationTests.swift
Normal file
168
camera_avfoundation/darwin/Tests/CameraOrientationTests.swift
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import Flutter
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
private final class MockUIDevice: UIDevice {
|
||||
var mockOrientation: UIDeviceOrientation = .unknown
|
||||
|
||||
override var orientation: UIDeviceOrientation {
|
||||
return mockOrientation
|
||||
}
|
||||
}
|
||||
|
||||
final class CameraOrientationTests: XCTestCase {
|
||||
private func createCameraPlugin() -> (
|
||||
cameraPlugin: CameraPlugin,
|
||||
mockCamera: MockCamera,
|
||||
mockEventAPI: MockGlobalEventApi,
|
||||
mockDevice: MockCaptureDevice,
|
||||
mockDeviceDiscoverer: MockCameraDeviceDiscoverer,
|
||||
captureSessionQueue: DispatchQueue
|
||||
) {
|
||||
let mockDevice = MockCaptureDevice()
|
||||
let mockCamera = MockCamera()
|
||||
let mockEventAPI = MockGlobalEventApi()
|
||||
let mockDeviceDiscoverer = MockCameraDeviceDiscoverer()
|
||||
let captureSessionQueue = DispatchQueue(label: "io.flutter.camera.captureSessionQueue")
|
||||
|
||||
let cameraPlugin = CameraPlugin(
|
||||
registry: MockFlutterTextureRegistry(),
|
||||
messenger: MockFlutterBinaryMessenger(),
|
||||
globalAPI: mockEventAPI,
|
||||
deviceDiscoverer: mockDeviceDiscoverer,
|
||||
permissionManager: MockCameraPermissionManager(),
|
||||
deviceFactory: { _ in mockDevice },
|
||||
captureSessionFactory: { MockCaptureSession() },
|
||||
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
|
||||
captureSessionQueue: captureSessionQueue
|
||||
)
|
||||
cameraPlugin.camera = mockCamera
|
||||
|
||||
return (
|
||||
cameraPlugin,
|
||||
mockCamera,
|
||||
mockEventAPI,
|
||||
mockDevice,
|
||||
mockDeviceDiscoverer,
|
||||
captureSessionQueue
|
||||
)
|
||||
}
|
||||
|
||||
private func sendOrientation(
|
||||
_ orientation: UIDeviceOrientation,
|
||||
to cameraPlugin: CameraPlugin,
|
||||
captureSessionQueue: DispatchQueue
|
||||
) {
|
||||
cameraPlugin.orientationChanged(createMockNotification(for: orientation))
|
||||
waitForQueueRoundTrip(with: captureSessionQueue)
|
||||
}
|
||||
|
||||
private func createMockNotification(for deviceOrientation: UIDeviceOrientation) -> Notification {
|
||||
let mockDevice = MockUIDevice()
|
||||
mockDevice.mockOrientation = deviceOrientation
|
||||
return Notification(name: Notification.Name("orientation_test"), object: mockDevice)
|
||||
}
|
||||
|
||||
func testOrientationNotifications() {
|
||||
let (cameraPlugin, _, mockEventAPI, _, _, captureSessionQueue) = createCameraPlugin()
|
||||
|
||||
sendOrientation(.portraitUpsideDown, to: cameraPlugin, captureSessionQueue: captureSessionQueue)
|
||||
XCTAssertEqual(mockEventAPI.lastOrientation, .portraitDown)
|
||||
sendOrientation(.portrait, to: cameraPlugin, captureSessionQueue: captureSessionQueue)
|
||||
XCTAssertEqual(mockEventAPI.lastOrientation, .portraitUp)
|
||||
sendOrientation(.landscapeLeft, to: cameraPlugin, captureSessionQueue: captureSessionQueue)
|
||||
XCTAssertEqual(mockEventAPI.lastOrientation, .landscapeLeft)
|
||||
sendOrientation(.landscapeRight, to: cameraPlugin, captureSessionQueue: captureSessionQueue)
|
||||
XCTAssertEqual(mockEventAPI.lastOrientation, .landscapeRight)
|
||||
}
|
||||
|
||||
func testOrientationNotificationsNotCalledForFaceUp() {
|
||||
let (cameraPlugin, _, mockEventAPI, _, _, captureSessionQueue) = createCameraPlugin()
|
||||
sendOrientation(.faceUp, to: cameraPlugin, captureSessionQueue: captureSessionQueue)
|
||||
XCTAssertFalse(mockEventAPI.deviceOrientationChangedCalled)
|
||||
}
|
||||
|
||||
func testOrientationNotificationsNotCalledForFaceDown() {
|
||||
let (cameraPlugin, _, mockEventAPI, _, _, captureSessionQueue) = createCameraPlugin()
|
||||
sendOrientation(.faceDown, to: cameraPlugin, captureSessionQueue: captureSessionQueue)
|
||||
XCTAssertFalse(mockEventAPI.deviceOrientationChangedCalled)
|
||||
}
|
||||
|
||||
func testOrientationUpdateMustBeOnCaptureSessionQueue() {
|
||||
let queueExpectation = expectation(
|
||||
description: "Orientation update must happen on the capture session queue")
|
||||
let (cameraPlugin, mockCamera, _, _, _, captureSessionQueue) = createCameraPlugin()
|
||||
let captureSessionQueueSpecific = DispatchSpecificKey<Void>()
|
||||
captureSessionQueue.setSpecific(
|
||||
key: captureSessionQueueSpecific,
|
||||
value: ())
|
||||
|
||||
mockCamera.setDeviceOrientationStub = { orientation in
|
||||
if DispatchQueue.getSpecific(key: captureSessionQueueSpecific) != nil {
|
||||
queueExpectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
cameraPlugin.orientationChanged(createMockNotification(for: .landscapeLeft))
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testOrientationChangedNoRetainCycle() {
|
||||
let (_, mockCamera, mockEventAPI, mockDevice, mockDeviceDiscoverer, _) = createCameraPlugin()
|
||||
let captureSessionQueue = DispatchQueue(label: "capture_session_queue")
|
||||
weak var weakPlugin: CameraPlugin?
|
||||
weak var weakDevice = mockDevice
|
||||
|
||||
autoreleasepool {
|
||||
let cameraPlugin = CameraPlugin(
|
||||
registry: MockFlutterTextureRegistry(),
|
||||
messenger: MockFlutterBinaryMessenger(),
|
||||
globalAPI: mockEventAPI,
|
||||
deviceDiscoverer: mockDeviceDiscoverer,
|
||||
permissionManager: MockCameraPermissionManager(),
|
||||
deviceFactory: { _ in weakDevice! },
|
||||
captureSessionFactory: { MockCaptureSession() },
|
||||
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
|
||||
captureSessionQueue: captureSessionQueue
|
||||
)
|
||||
weakPlugin = cameraPlugin
|
||||
cameraPlugin.camera = mockCamera
|
||||
|
||||
cameraPlugin.orientationChanged(createMockNotification(for: .landscapeLeft))
|
||||
}
|
||||
|
||||
// Sanity check.
|
||||
let cameraDeallocatedExpectation = self.expectation(
|
||||
description: "Camera must have been deallocated.")
|
||||
captureSessionQueue.async {
|
||||
XCTAssertNil(weakPlugin)
|
||||
cameraDeallocatedExpectation.fulfill()
|
||||
}
|
||||
// Awaiting expectation is needed. The test is flaky when checking for nil right away.
|
||||
waitForExpectations(timeout: 1, handler: nil)
|
||||
|
||||
var setDeviceOrientationCalled = false
|
||||
mockCamera.setDeviceOrientationStub = { orientation in
|
||||
if orientation == .landscapeLeft {
|
||||
setDeviceOrientationCalled = true
|
||||
}
|
||||
}
|
||||
|
||||
weak var weakEventAPI = mockEventAPI
|
||||
// Must check in captureSessionQueue since orientationChanged dispatches to this queue.
|
||||
let expectation = self.expectation(description: "Dispatched to capture session queue")
|
||||
captureSessionQueue.async {
|
||||
XCTAssertFalse(setDeviceOrientationCalled)
|
||||
XCTAssertFalse(weakEventAPI?.deviceOrientationChangedCalled ?? false)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
}
|
||||
237
camera_avfoundation/darwin/Tests/CameraPermissionTests.swift
Normal file
237
camera_avfoundation/darwin/Tests/CameraPermissionTests.swift
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import Flutter
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
private final class MockPermissionService: NSObject, PermissionServicing {
|
||||
var authorizationStatusStub: ((AVMediaType) -> AVAuthorizationStatus)?
|
||||
var requestAccessStub: ((AVMediaType, @escaping @Sendable (Bool) -> Void) -> Void)?
|
||||
|
||||
func authorizationStatus(for mediaType: AVMediaType) -> AVAuthorizationStatus {
|
||||
return authorizationStatusStub?(mediaType) ?? .notDetermined
|
||||
}
|
||||
|
||||
func requestAccess(for mediaType: AVMediaType, completion: @escaping @Sendable (Bool) -> Void) {
|
||||
requestAccessStub?(mediaType, completion)
|
||||
}
|
||||
}
|
||||
|
||||
final class CameraPermissionManagerTests: XCTestCase {
|
||||
private func createSutAndMocks() -> (CameraPermissionManager, MockPermissionService) {
|
||||
let mockPermissionService = MockPermissionService()
|
||||
let permissionManager = CameraPermissionManager(permissionService: mockPermissionService)
|
||||
|
||||
return (permissionManager, mockPermissionService)
|
||||
}
|
||||
|
||||
// MARK: - Camera permissions
|
||||
|
||||
func testRequestCameraPermission_completeWithoutErrorIfPreviouslyAuthorized() {
|
||||
let (permissionManager, mockPermissionService) = createSutAndMocks()
|
||||
let expectation = self.expectation(
|
||||
description: "Must complete without error if camera access was previously authorized.")
|
||||
|
||||
mockPermissionService.authorizationStatusStub = { mediaType in
|
||||
XCTAssertEqual(mediaType, .video)
|
||||
return .authorized
|
||||
}
|
||||
permissionManager.requestCameraPermission { error in
|
||||
XCTAssertNil(error)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testRequestCameraPermission_completeWithErrorIfPreviouslyDenied() {
|
||||
let (permissionManager, mockPermissionService) = createSutAndMocks()
|
||||
let expectation = self.expectation(
|
||||
description: "Must complete with error if camera access was previously denied.")
|
||||
|
||||
mockPermissionService.authorizationStatusStub = { mediaType in
|
||||
XCTAssertEqual(mediaType, .video)
|
||||
return .denied
|
||||
}
|
||||
permissionManager.requestCameraPermission { error in
|
||||
XCTAssertEqual(error?.code, "CameraAccessDeniedWithoutPrompt")
|
||||
XCTAssertEqual(
|
||||
error?.message,
|
||||
"User has previously denied the camera access request. Go to Settings to enable camera access."
|
||||
)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testRequestCameraPermission_completeWithErrorIfRestricted() {
|
||||
let (permissionManager, mockPermissionService) = createSutAndMocks()
|
||||
let expectation = self.expectation(
|
||||
description: "Must complete with error if camera access is restricted.")
|
||||
|
||||
mockPermissionService.authorizationStatusStub = { mediaType in
|
||||
XCTAssertEqual(mediaType, .video)
|
||||
return .restricted
|
||||
}
|
||||
permissionManager.requestCameraPermission { error in
|
||||
XCTAssertEqual(error?.code, "CameraAccessRestricted")
|
||||
XCTAssertEqual(error?.message, "Camera access is restricted.")
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testRequestCameraPermission_completeWithoutErrorIfUserGrantAccess() {
|
||||
let (permissionManager, mockPermissionService) = createSutAndMocks()
|
||||
let expectation = self.expectation(
|
||||
description: "Must complete without error if user granted access.")
|
||||
|
||||
mockPermissionService.authorizationStatusStub = { mediaType in
|
||||
XCTAssertEqual(mediaType, .video)
|
||||
return .notDetermined
|
||||
}
|
||||
mockPermissionService.requestAccessStub = { mediaType, handler in
|
||||
XCTAssertEqual(mediaType, .video)
|
||||
// Grant access.
|
||||
handler(true)
|
||||
}
|
||||
permissionManager.requestCameraPermission { error in
|
||||
XCTAssertNil(error)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testRequestCameraPermission_completeWithErrorIfUserDenyAccess() {
|
||||
let (permissionManager, mockPermissionService) = createSutAndMocks()
|
||||
let expectation = self.expectation(
|
||||
description: "Must complete with error if user denied access.")
|
||||
|
||||
mockPermissionService.authorizationStatusStub = { mediaType in
|
||||
XCTAssertEqual(mediaType, .video)
|
||||
return .notDetermined
|
||||
}
|
||||
mockPermissionService.requestAccessStub = { mediaType, handler in
|
||||
XCTAssertEqual(mediaType, .video)
|
||||
// Deny access.
|
||||
handler(false)
|
||||
}
|
||||
permissionManager.requestCameraPermission { error in
|
||||
XCTAssertEqual(error?.code, "CameraAccessDenied")
|
||||
XCTAssertEqual(error?.message, "User denied the camera access request.")
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
// MARK: - Audio permissions
|
||||
|
||||
func testRequestAudioPermission_completeWithoutErrorIfPreviouslyAuthorized() {
|
||||
let (permissionManager, mockPermissionService) = createSutAndMocks()
|
||||
let expectation = self.expectation(
|
||||
description: "Must complete without error if audio access was previously authorized.")
|
||||
|
||||
mockPermissionService.authorizationStatusStub = { mediaType in
|
||||
XCTAssertEqual(mediaType, .audio)
|
||||
return .authorized
|
||||
}
|
||||
permissionManager.requestAudioPermission { error in
|
||||
XCTAssertNil(error)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testRequestAudioPermission_completeWithErrorIfPreviouslyDenied() {
|
||||
let (permissionManager, mockPermissionService) = createSutAndMocks()
|
||||
let expectation = self.expectation(
|
||||
description: "Must complete with error if audio access was previously denied.")
|
||||
|
||||
mockPermissionService.authorizationStatusStub = { mediaType in
|
||||
XCTAssertEqual(mediaType, .audio)
|
||||
return .denied
|
||||
}
|
||||
permissionManager.requestAudioPermission { error in
|
||||
XCTAssertEqual(error?.code, "AudioAccessDeniedWithoutPrompt")
|
||||
XCTAssertEqual(
|
||||
error?.message,
|
||||
"User has previously denied the audio access request. Go to Settings to enable audio access."
|
||||
)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testRequestAudioPermission_completeWithErrorIfRestricted() {
|
||||
let (permissionManager, mockPermissionService) = createSutAndMocks()
|
||||
let expectation = self.expectation(
|
||||
description: "Must complete with error if audio access is restricted.")
|
||||
|
||||
mockPermissionService.authorizationStatusStub = { mediaType in
|
||||
XCTAssertEqual(mediaType, .audio)
|
||||
return .restricted
|
||||
}
|
||||
permissionManager.requestAudioPermission { error in
|
||||
XCTAssertEqual(error?.code, "AudioAccessRestricted")
|
||||
XCTAssertEqual(error?.message, "Audio access is restricted.")
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testRequestAudioPermission_completeWithoutErrorIfUserGrantAccess() {
|
||||
let (permissionManager, mockPermissionService) = createSutAndMocks()
|
||||
let expectation = self.expectation(
|
||||
description: "Must complete without error if user granted access.")
|
||||
|
||||
mockPermissionService.authorizationStatusStub = { mediaType in
|
||||
XCTAssertEqual(mediaType, .audio)
|
||||
return .notDetermined
|
||||
}
|
||||
mockPermissionService.requestAccessStub = { mediaType, handler in
|
||||
XCTAssertEqual(mediaType, .audio)
|
||||
// Grant access.
|
||||
handler(true)
|
||||
}
|
||||
permissionManager.requestAudioPermission { error in
|
||||
XCTAssertNil(error)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testRequestAudioPermission_completeWithErrorIfUserDenyAccess() {
|
||||
let (permissionManager, mockPermissionService) = createSutAndMocks()
|
||||
let expectation = self.expectation(
|
||||
description: "Must complete with error if user denied access")
|
||||
|
||||
mockPermissionService.authorizationStatusStub = { mediaType in
|
||||
XCTAssertEqual(mediaType, .audio)
|
||||
return .notDetermined
|
||||
}
|
||||
mockPermissionService.requestAccessStub = { mediaType, handler in
|
||||
XCTAssertEqual(mediaType, .audio)
|
||||
// Deny access.
|
||||
handler(false)
|
||||
}
|
||||
permissionManager.requestAudioPermission { error in
|
||||
XCTAssertEqual(error?.code, "AudioAccessDenied")
|
||||
XCTAssertEqual(error?.message, "User denied the audio access request.")
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class CameraPluginCreateCameraTests: XCTestCase {
|
||||
private func createCameraPlugin() -> (
|
||||
CameraPlugin, MockCameraPermissionManager, MockCaptureSession
|
||||
) {
|
||||
let mockPermissionManager = MockCameraPermissionManager()
|
||||
let mockCaptureSession = MockCaptureSession()
|
||||
|
||||
let cameraPlugin = CameraPlugin(
|
||||
registry: MockFlutterTextureRegistry(),
|
||||
messenger: MockFlutterBinaryMessenger(),
|
||||
globalAPI: MockGlobalEventApi(),
|
||||
deviceDiscoverer: MockCameraDeviceDiscoverer(),
|
||||
permissionManager: mockPermissionManager,
|
||||
deviceFactory: { _ in MockCaptureDevice() },
|
||||
captureSessionFactory: { mockCaptureSession },
|
||||
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
|
||||
captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue")
|
||||
)
|
||||
|
||||
return (cameraPlugin, mockPermissionManager, mockCaptureSession)
|
||||
}
|
||||
|
||||
func testCreateCamera_requestsOnlyCameraPermissionWithAudioDisabled() {
|
||||
let (cameraPlugin, mockPermissionManager, _) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Initialization completed")
|
||||
|
||||
var requestCameraPermissionCalled = false
|
||||
mockPermissionManager.requestCameraPermissionStub = { completion in
|
||||
requestCameraPermissionCalled = true
|
||||
// Permission is granted
|
||||
completion(nil)
|
||||
}
|
||||
var requestAudioPermissionCalled = false
|
||||
mockPermissionManager.requestAudioPermissionStub = { completion in
|
||||
requestAudioPermissionCalled = true
|
||||
// Permission is granted
|
||||
completion(nil)
|
||||
}
|
||||
|
||||
cameraPlugin.create(
|
||||
cameraName: "camera_name",
|
||||
settings: PlatformMediaSettings(
|
||||
resolutionPreset: .medium,
|
||||
framesPerSecond: nil,
|
||||
videoBitrate: nil,
|
||||
audioBitrate: nil,
|
||||
enableAudio: false)
|
||||
) { result in
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(requestCameraPermissionCalled)
|
||||
XCTAssertFalse(requestAudioPermissionCalled)
|
||||
}
|
||||
|
||||
func testCreateCamera_requestsCameraAndAudioPermissionWithAudioEnabled() {
|
||||
let (cameraPlugin, mockPermissionManager, _) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Initialization completed")
|
||||
|
||||
var requestCameraPermissionCalled = false
|
||||
mockPermissionManager.requestCameraPermissionStub = { completion in
|
||||
requestCameraPermissionCalled = true
|
||||
// Permission is granted
|
||||
completion(nil)
|
||||
}
|
||||
var requestAudioPermissionCalled = false
|
||||
mockPermissionManager.requestAudioPermissionStub = { completion in
|
||||
requestAudioPermissionCalled = true
|
||||
// Permission is granted
|
||||
completion(nil)
|
||||
}
|
||||
|
||||
cameraPlugin.create(
|
||||
cameraName: "camera_name",
|
||||
settings: PlatformMediaSettings(
|
||||
resolutionPreset: .medium,
|
||||
framesPerSecond: nil,
|
||||
videoBitrate: nil,
|
||||
audioBitrate: nil,
|
||||
enableAudio: true)
|
||||
) { result in
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(requestCameraPermissionCalled)
|
||||
XCTAssertTrue(requestAudioPermissionCalled)
|
||||
}
|
||||
|
||||
func testCreateCamera_createsCameraSuccessfully() {
|
||||
let (cameraPlugin, mockPermissionManager, mockCaptureSession) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Initialization completed")
|
||||
|
||||
mockPermissionManager.requestCameraPermissionStub = { completion in
|
||||
// Permission is granted
|
||||
completion(nil)
|
||||
}
|
||||
mockPermissionManager.requestAudioPermissionStub = { completion in
|
||||
// Permission is granted
|
||||
completion(nil)
|
||||
}
|
||||
mockCaptureSession.canSetSessionPresetStub = { _ in true }
|
||||
|
||||
cameraPlugin.create(
|
||||
cameraName: "camera_name",
|
||||
settings: PlatformMediaSettings(
|
||||
resolutionPreset: .medium,
|
||||
framesPerSecond: nil,
|
||||
videoBitrate: nil,
|
||||
audioBitrate: nil,
|
||||
enableAudio: true)
|
||||
) { result in
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertNotNil(cameraPlugin.camera)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,652 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// Tests of `CameraPlugin` methods delegating to `Camera` instance
|
||||
final class CameraPluginDelegatingMethodTests: XCTestCase {
|
||||
private func createCameraPlugin() -> (CameraPlugin, MockCamera) {
|
||||
let mockCamera = MockCamera()
|
||||
|
||||
let cameraPlugin = CameraPlugin(
|
||||
registry: MockFlutterTextureRegistry(),
|
||||
messenger: MockFlutterBinaryMessenger(),
|
||||
globalAPI: MockGlobalEventApi(),
|
||||
deviceDiscoverer: MockCameraDeviceDiscoverer(),
|
||||
permissionManager: MockCameraPermissionManager(),
|
||||
deviceFactory: { _ in MockCaptureDevice() },
|
||||
captureSessionFactory: { MockCaptureSession() },
|
||||
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
|
||||
captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue")
|
||||
)
|
||||
cameraPlugin.camera = mockCamera
|
||||
|
||||
return (cameraPlugin, mockCamera)
|
||||
}
|
||||
|
||||
func testLockCapture_callsCameraLockCapture() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
let targetOrientation = PlatformDeviceOrientation.landscapeLeft
|
||||
|
||||
var lockCaptureCalled = false
|
||||
mockCamera.lockCaptureOrientationStub = { orientation in
|
||||
XCTAssertEqual(orientation, targetOrientation)
|
||||
lockCaptureCalled = true
|
||||
}
|
||||
|
||||
cameraPlugin.lockCaptureOrientation(orientation: targetOrientation) { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(lockCaptureCalled)
|
||||
}
|
||||
|
||||
func testPausePreview_callsCameraPausePreview() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
var pausePreviewCalled = false
|
||||
mockCamera.pausePreviewStub = {
|
||||
pausePreviewCalled = true
|
||||
}
|
||||
|
||||
cameraPlugin.pausePreview { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(pausePreviewCalled)
|
||||
}
|
||||
|
||||
func testPauseVideoRecording_callsCameraPauseVideoRecording() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
var pauseVideoRecordingCalled = false
|
||||
mockCamera.pauseVideoRecordingStub = {
|
||||
pauseVideoRecordingCalled = true
|
||||
}
|
||||
|
||||
cameraPlugin.pauseVideoRecording { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(pauseVideoRecordingCalled)
|
||||
}
|
||||
|
||||
func testPrepareForVideoRecording_callsCameraSetUpCaptureSessionForAudioIfNeeded() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
var setUpCaptureSessionForAudioIfNeededCalled = false
|
||||
mockCamera.setUpCaptureSessionForAudioIfNeededStub = {
|
||||
setUpCaptureSessionForAudioIfNeededCalled = true
|
||||
}
|
||||
|
||||
cameraPlugin.prepareForVideoRecording { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(setUpCaptureSessionForAudioIfNeededCalled)
|
||||
}
|
||||
|
||||
func testReceivedImageStreamData_callsCameraReceivedImageStreamData() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
var receivedImageStreamDataCalled = false
|
||||
mockCamera.receivedImageStreamDataStub = {
|
||||
receivedImageStreamDataCalled = true
|
||||
}
|
||||
|
||||
cameraPlugin.receivedImageStreamData { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(receivedImageStreamDataCalled)
|
||||
}
|
||||
|
||||
func testResumeVideoRecording_callsCameraResumeVideoRecording() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
var resumeVideoRecordingCalled = false
|
||||
mockCamera.resumeVideoRecordingStub = {
|
||||
resumeVideoRecordingCalled = true
|
||||
}
|
||||
|
||||
cameraPlugin.resumeVideoRecording { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(resumeVideoRecordingCalled)
|
||||
}
|
||||
|
||||
func testResumePreview_callsCameraResumePreview() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
var resumePreviewCalled = false
|
||||
mockCamera.resumePreviewStub = {
|
||||
resumePreviewCalled = true
|
||||
}
|
||||
|
||||
cameraPlugin.resumePreview { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(resumePreviewCalled)
|
||||
}
|
||||
|
||||
func testSetExposureMode_callsCameraExposureMode() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
let targetExposureMode = PlatformExposureMode.locked
|
||||
|
||||
var setExposureModeCalled = false
|
||||
mockCamera.setExposureModeStub = { mode in
|
||||
XCTAssertEqual(mode, targetExposureMode)
|
||||
setExposureModeCalled = true
|
||||
}
|
||||
|
||||
cameraPlugin.setExposureMode(mode: targetExposureMode) { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(setExposureModeCalled)
|
||||
}
|
||||
|
||||
func testSetExposureOffset_callsCameraSetExposureOffset() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
let targetExposureOffset = 1.0
|
||||
|
||||
var setExposureOffsetCalled = false
|
||||
mockCamera.setExposureOffsetStub = { offset in
|
||||
XCTAssertEqual(offset, targetExposureOffset)
|
||||
setExposureOffsetCalled = true
|
||||
}
|
||||
|
||||
cameraPlugin.setExposureOffset(offset: targetExposureOffset) { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(setExposureOffsetCalled)
|
||||
}
|
||||
|
||||
func testSetFocusMode_callsCameraSetFocusMode() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
let targetFocusMode = PlatformFocusMode.locked
|
||||
|
||||
var setFocusModeCalled = false
|
||||
mockCamera.setFocusModeStub = { mode in
|
||||
XCTAssertEqual(mode, targetFocusMode)
|
||||
setFocusModeCalled = true
|
||||
}
|
||||
|
||||
cameraPlugin.setFocusMode(mode: targetFocusMode) { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(setFocusModeCalled)
|
||||
}
|
||||
|
||||
func testSetImageFileFormat_callsCameraSetImageFileFormat() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
let targetFileFormat = PlatformImageFileFormat.heif
|
||||
|
||||
var setImageFileFormatCalled = false
|
||||
mockCamera.setImageFileFormatStub = { fileFormat in
|
||||
XCTAssertEqual(fileFormat, targetFileFormat)
|
||||
setImageFileFormatCalled = true
|
||||
}
|
||||
|
||||
cameraPlugin.setImageFileFormat(format: targetFileFormat) { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(setImageFileFormatCalled)
|
||||
}
|
||||
|
||||
func testSetImageQuality_callsCameraSetImageQuality() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
let targetQuality: Int64 = 50
|
||||
|
||||
var setJpegImageQualityCalled = false
|
||||
mockCamera.setJpegImageQualityStub = { quality in
|
||||
XCTAssertEqual(quality, targetQuality)
|
||||
setJpegImageQualityCalled = true
|
||||
}
|
||||
|
||||
cameraPlugin.setJpegImageQuality(quality: targetQuality) { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(setJpegImageQualityCalled)
|
||||
}
|
||||
|
||||
func testStartImageStream_callsCameraStartImageStream() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
var startImageStreamCalled = false
|
||||
mockCamera.startImageStreamStub = { messenger, completion in
|
||||
startImageStreamCalled = true
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
cameraPlugin.startImageStream { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(startImageStreamCalled)
|
||||
}
|
||||
|
||||
func testStopImageStream_callsCameraStopImageStream() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
var stopImageStreamCalled = false
|
||||
mockCamera.stopImageStreamStub = {
|
||||
stopImageStreamCalled = true
|
||||
}
|
||||
|
||||
cameraPlugin.stopImageStream { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(stopImageStreamCalled)
|
||||
}
|
||||
|
||||
func testStartVideoRecording_withStreamingTrue_callsCameraStartVideoRecordingWithMessenger() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
var startVideoRecordingCalled = false
|
||||
mockCamera.startVideoRecordingStub = { completion, messenger in
|
||||
XCTAssertNotNil(messenger)
|
||||
startVideoRecordingCalled = true
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
cameraPlugin.startVideoRecording(enableStream: true) { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(startVideoRecordingCalled)
|
||||
}
|
||||
|
||||
func testStartVideoRecording_withStreamingFalse_callsCameraStartVideoRecordingWithoutMessenger() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
var startVideoRecordingCalled = false
|
||||
mockCamera.startVideoRecordingStub = { completion, messenger in
|
||||
XCTAssertNil(messenger)
|
||||
startVideoRecordingCalled = true
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
cameraPlugin.startVideoRecording(enableStream: false) { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(startVideoRecordingCalled)
|
||||
}
|
||||
|
||||
func testStopVideoRecording_callsCameraStopVideoRecording() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
let targetPath = "path"
|
||||
|
||||
var stopVideoRecordingCalled = false
|
||||
mockCamera.stopVideoRecordingStub = { completion in
|
||||
stopVideoRecordingCalled = true
|
||||
completion(.success(targetPath))
|
||||
}
|
||||
|
||||
cameraPlugin.stopVideoRecording { result in
|
||||
switch result {
|
||||
case .success(let path):
|
||||
XCTAssertEqual(path, targetPath)
|
||||
case .failure:
|
||||
XCTFail("Unexpected error")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(stopVideoRecordingCalled)
|
||||
}
|
||||
|
||||
func testUnlockCaptureOrientation_callsCameraUnlockCaptureOrientation() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
var unlockCaptureOrientationCalled = false
|
||||
mockCamera.unlockCaptureOrientationStub = {
|
||||
unlockCaptureOrientationCalled = true
|
||||
}
|
||||
|
||||
cameraPlugin.unlockCaptureOrientation { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(unlockCaptureOrientationCalled)
|
||||
}
|
||||
|
||||
func testSetExposurePoint_callsCameraSetExposurePoint() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
let targetExposurePoint = PlatformPoint(x: 1.0, y: 1.0)
|
||||
|
||||
var setExposurePointCalled = false
|
||||
mockCamera.setExposurePointStub = { point, completion in
|
||||
XCTAssertEqual(point, targetExposurePoint)
|
||||
setExposurePointCalled = true
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
cameraPlugin.setExposurePoint(point: targetExposurePoint) { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(setExposurePointCalled)
|
||||
}
|
||||
|
||||
func testSetFlashMode_callsCameraSetFlashMode() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
let targetFlashMode = PlatformFlashMode.auto
|
||||
|
||||
var setFlashModeCalled = false
|
||||
mockCamera.setFlashModeStub = { mode, completion in
|
||||
XCTAssertEqual(mode, targetFlashMode)
|
||||
setFlashModeCalled = true
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
cameraPlugin.setFlashMode(mode: targetFlashMode) { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(setFlashModeCalled)
|
||||
}
|
||||
|
||||
func testSetFocusPoint_callsCameraSetFocusPoint() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
let targetFocusPoint = PlatformPoint(x: 1.0, y: 1.0)
|
||||
|
||||
var setFocusPointCalled = false
|
||||
mockCamera.setFocusPointStub = { point, completion in
|
||||
XCTAssertEqual(point, targetFocusPoint)
|
||||
setFocusPointCalled = true
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
cameraPlugin.setFocusPoint(point: targetFocusPoint) { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(setFocusPointCalled)
|
||||
}
|
||||
|
||||
func testSetZoomLevel_callsCameraSetZoomLevel() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
let targetZoomLevel = 1.0
|
||||
|
||||
var setZoomLevelCalled = false
|
||||
mockCamera.setZoomLevelStub = { zoom, completion in
|
||||
XCTAssertEqual(zoom, targetZoomLevel)
|
||||
setZoomLevelCalled = true
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
cameraPlugin.setZoomLevel(zoom: targetZoomLevel) { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(setZoomLevelCalled)
|
||||
}
|
||||
|
||||
func testTakePicture_callsCameraCaptureToFile() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
let targetPath = "path"
|
||||
|
||||
var captureToFileCalled = false
|
||||
mockCamera.captureToFileStub = { completion in
|
||||
captureToFileCalled = true
|
||||
completion(.success(targetPath))
|
||||
}
|
||||
|
||||
cameraPlugin.takePicture { result in
|
||||
switch result {
|
||||
case .success(let path):
|
||||
XCTAssertEqual(path, targetPath)
|
||||
case .failure:
|
||||
XCTFail("Unexpected error")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(captureToFileCalled)
|
||||
}
|
||||
|
||||
func testUpdateDescriptionWhileRecording_callsCameraSetDescriptionWhileRecording() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
let targetCameraName = "camera_name"
|
||||
|
||||
var setDescriptionWhileRecordingCalled = false
|
||||
mockCamera.setDescriptionWhileRecordingStub = { cameraName, completion in
|
||||
XCTAssertEqual(cameraName, targetCameraName)
|
||||
setDescriptionWhileRecordingCalled = true
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
cameraPlugin.updateDescriptionWhileRecording(cameraName: targetCameraName) { result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(setDescriptionWhileRecordingCalled)
|
||||
}
|
||||
|
||||
func testGetMaxZoomLevel_returnsValueFromCameraGetMaximumAvailableZoomFactor() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
let targetMaximumZoomLevel = CGFloat(1.0)
|
||||
|
||||
var getMaximumAvailableZoomFactorCalled = false
|
||||
mockCamera.getMaximumAvailableZoomFactorStub = {
|
||||
getMaximumAvailableZoomFactorCalled = true
|
||||
return targetMaximumZoomLevel
|
||||
}
|
||||
|
||||
cameraPlugin.getMaxZoomLevel { result in
|
||||
switch result {
|
||||
case .success(let zoom):
|
||||
XCTAssertEqual(zoom, targetMaximumZoomLevel)
|
||||
case .failure:
|
||||
XCTFail("Unexpected error")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(getMaximumAvailableZoomFactorCalled)
|
||||
}
|
||||
|
||||
func testGetMinZoomLevel_returnsValueFromCameraGetMinimumAvailableZoomFactor() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
let targetMinimumZoomLevel = CGFloat(1.0)
|
||||
|
||||
var getMinimumAvailableZoomFactorCalled = false
|
||||
mockCamera.getMinimumAvailableZoomFactorStub = {
|
||||
getMinimumAvailableZoomFactorCalled = true
|
||||
return targetMinimumZoomLevel
|
||||
}
|
||||
|
||||
cameraPlugin.getMinZoomLevel { result in
|
||||
switch result {
|
||||
case .success(let zoom):
|
||||
XCTAssertEqual(zoom, targetMinimumZoomLevel)
|
||||
case .failure:
|
||||
XCTFail("Unexpected error")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(getMinimumAvailableZoomFactorCalled)
|
||||
}
|
||||
|
||||
func testGetMaxExposureOffset_returnsValueFromCameraGetMaximumExposureOffset() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
let targetMaximumExposureOffset = CGFloat(1.0)
|
||||
|
||||
var getMaximumExposureOffsetCalled = false
|
||||
mockCamera.getMaximumExposureOffsetStub = {
|
||||
getMaximumExposureOffsetCalled = true
|
||||
return targetMaximumExposureOffset
|
||||
}
|
||||
|
||||
cameraPlugin.getMaxExposureOffset { result in
|
||||
switch result {
|
||||
case .success(let offset):
|
||||
XCTAssertEqual(offset, targetMaximumExposureOffset)
|
||||
case .failure:
|
||||
XCTFail("Unexpected error")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(getMaximumExposureOffsetCalled)
|
||||
}
|
||||
|
||||
func testGetMinExposureOffset_returnsValueFromCameraGetMinimumExposureOffset() {
|
||||
let (cameraPlugin, mockCamera) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
let targetMinimumExposureOffset = CGFloat(1.0)
|
||||
|
||||
var getMinimumExposureOffsetCalled = false
|
||||
mockCamera.getMinimumExposureOffsetStub = {
|
||||
getMinimumExposureOffsetCalled = true
|
||||
return targetMinimumExposureOffset
|
||||
}
|
||||
|
||||
cameraPlugin.getMinExposureOffset { result in
|
||||
switch result {
|
||||
case .success(let offset):
|
||||
XCTAssertEqual(offset, targetMinimumExposureOffset)
|
||||
case .failure:
|
||||
XCTFail("Unexpected error")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(getMinimumExposureOffsetCalled)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class CameraPluginInitializeCameraTests: XCTestCase {
|
||||
private func createCameraPlugin() -> (
|
||||
CameraPlugin, MockCamera, MockGlobalEventApi, DispatchQueue
|
||||
) {
|
||||
let mockCamera = MockCamera()
|
||||
let mockGlobalEventApi = MockGlobalEventApi()
|
||||
let captureSessionQueue = DispatchQueue(label: "io.flutter.camera.captureSessionQueue")
|
||||
|
||||
let cameraPlugin = CameraPlugin(
|
||||
registry: MockFlutterTextureRegistry(),
|
||||
messenger: MockFlutterBinaryMessenger(),
|
||||
globalAPI: mockGlobalEventApi,
|
||||
deviceDiscoverer: MockCameraDeviceDiscoverer(),
|
||||
permissionManager: MockCameraPermissionManager(),
|
||||
deviceFactory: { _ in MockCaptureDevice() },
|
||||
captureSessionFactory: { MockCaptureSession() },
|
||||
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
|
||||
captureSessionQueue: captureSessionQueue
|
||||
)
|
||||
cameraPlugin.camera = mockCamera
|
||||
|
||||
return (cameraPlugin, mockCamera, mockGlobalEventApi, captureSessionQueue)
|
||||
}
|
||||
|
||||
func testInitializeCamera_setsCameraOnFrameAvailableCallback() {
|
||||
let (cameraPlugin, mockCamera, _, _) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Initialization completed")
|
||||
|
||||
var onFrameAvailableSet = false
|
||||
mockCamera.setOnFrameAvailableStub = { callback in
|
||||
onFrameAvailableSet = true
|
||||
}
|
||||
|
||||
cameraPlugin.initialize(cameraId: 0, imageFormat: PlatformImageFormatGroup.bgra8888) {
|
||||
result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(onFrameAvailableSet)
|
||||
}
|
||||
|
||||
func testInitializeCamera_setsCameraDartAPI() {
|
||||
let (cameraPlugin, mockCamera, _, _) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Initialization completed")
|
||||
|
||||
var dartAPISet = false
|
||||
mockCamera.setDartApiStub = { api in
|
||||
dartAPISet = true
|
||||
}
|
||||
|
||||
cameraPlugin.initialize(cameraId: 0, imageFormat: PlatformImageFormatGroup.bgra8888) {
|
||||
result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(dartAPISet)
|
||||
}
|
||||
|
||||
func testInitializeCamera_sendsDeviceOrientation() {
|
||||
let (cameraPlugin, _, mockGlobalEventApi, captureSessionQueue) = createCameraPlugin()
|
||||
|
||||
cameraPlugin.initialize(cameraId: 0, imageFormat: PlatformImageFormatGroup.bgra8888) {
|
||||
result in
|
||||
let _ = self.assertSuccess(result)
|
||||
}
|
||||
|
||||
waitForQueueRoundTrip(with: captureSessionQueue)
|
||||
|
||||
XCTAssertTrue(mockGlobalEventApi.deviceOrientationChangedCalled)
|
||||
}
|
||||
|
||||
func testInitializeCamera_startsCamera() {
|
||||
let (cameraPlugin, mockCamera, _, _) = createCameraPlugin()
|
||||
let expectation = expectation(description: "Initialization completed")
|
||||
|
||||
var startCalled = false
|
||||
mockCamera.startStub = {
|
||||
startCalled = true
|
||||
}
|
||||
|
||||
cameraPlugin.initialize(cameraId: 0, imageFormat: PlatformImageFormatGroup.bgra8888) {
|
||||
result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
XCTAssertTrue(startCalled)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class CameraPreviewPauseTests: XCTestCase {
|
||||
func testPausePreviewWithResult_shouldPausePreview() {
|
||||
let camera = CameraTestUtils.createTestCamera()
|
||||
|
||||
camera.pausePreview()
|
||||
|
||||
XCTAssertTrue(camera.isPreviewPaused)
|
||||
}
|
||||
|
||||
func testResumePreviewWithResult_shouldResumePreview() {
|
||||
let camera = CameraTestUtils.createTestCamera()
|
||||
|
||||
camera.resumePreview()
|
||||
|
||||
XCTAssertFalse(camera.isPreviewPaused)
|
||||
}
|
||||
}
|
||||
73
camera_avfoundation/darwin/Tests/CameraPropertiesTests.swift
Normal file
73
camera_avfoundation/darwin/Tests/CameraPropertiesTests.swift
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class CameraPropertiesTests: XCTestCase {
|
||||
// MARK: - Flash Mode Tests
|
||||
|
||||
func testGetAVCaptureFlashModeForPigeonFlashMode() {
|
||||
XCTAssertEqual(
|
||||
AVCaptureDevice.FlashMode.off,
|
||||
getAVCaptureFlashMode(for: .off))
|
||||
XCTAssertEqual(
|
||||
AVCaptureDevice.FlashMode.auto,
|
||||
getAVCaptureFlashMode(for: .auto))
|
||||
XCTAssertEqual(
|
||||
AVCaptureDevice.FlashMode.on,
|
||||
getAVCaptureFlashMode(for: .always))
|
||||
}
|
||||
|
||||
// MARK: - Video Format Tests
|
||||
|
||||
func testGetPixelFormatForPigeonFormat() {
|
||||
XCTAssertEqual(
|
||||
kCVPixelFormatType_32BGRA,
|
||||
getPixelFormat(for: .bgra8888))
|
||||
XCTAssertEqual(
|
||||
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
|
||||
getPixelFormat(for: .yuv420))
|
||||
}
|
||||
|
||||
// MARK: - Device Orientation Tests
|
||||
|
||||
func testGetUIDeviceOrientationForPigeonDeviceOrientation() {
|
||||
XCTAssertEqual(
|
||||
UIDeviceOrientation.portraitUpsideDown,
|
||||
getUIDeviceOrientation(for: .portraitDown)
|
||||
)
|
||||
XCTAssertEqual(
|
||||
UIDeviceOrientation.landscapeLeft,
|
||||
getUIDeviceOrientation(for: .landscapeLeft))
|
||||
XCTAssertEqual(
|
||||
UIDeviceOrientation.landscapeRight,
|
||||
getUIDeviceOrientation(for: .landscapeRight))
|
||||
XCTAssertEqual(
|
||||
UIDeviceOrientation.portrait,
|
||||
getUIDeviceOrientation(for: .portraitUp))
|
||||
}
|
||||
|
||||
func testGetPigeonDeviceOrientationForUIDeviceOrientation() {
|
||||
XCTAssertEqual(
|
||||
PlatformDeviceOrientation.portraitDown,
|
||||
getPigeonDeviceOrientation(for: .portraitUpsideDown))
|
||||
XCTAssertEqual(
|
||||
PlatformDeviceOrientation.landscapeLeft,
|
||||
getPigeonDeviceOrientation(for: .landscapeLeft))
|
||||
XCTAssertEqual(
|
||||
PlatformDeviceOrientation.landscapeRight,
|
||||
getPigeonDeviceOrientation(for: .landscapeRight))
|
||||
XCTAssertEqual(
|
||||
PlatformDeviceOrientation.portraitUp,
|
||||
getPigeonDeviceOrientation(for: .portrait))
|
||||
// Test default case.
|
||||
XCTAssertEqual(
|
||||
PlatformDeviceOrientation.portraitUp,
|
||||
getPigeonDeviceOrientation(for: .unknown))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// Includes test cases related to resolution presets setting operations for Camera class.
|
||||
final class CameraSessionPresetsTests: XCTestCase {
|
||||
func testResolutionPresetWithBestFormat_mustUpdateCaptureSessionPreset() {
|
||||
let expectedPreset = AVCaptureSession.Preset.inputPriority
|
||||
let presetExpectation = expectation(description: "Expected preset set")
|
||||
let lockForConfigurationExpectation = expectation(
|
||||
description: "Expected lockForConfiguration called")
|
||||
|
||||
let videoSessionMock = MockCaptureSession()
|
||||
videoSessionMock.canSetSessionPresetStub = { _ in true }
|
||||
videoSessionMock.setSessionPresetStub = { preset in
|
||||
if preset == expectedPreset {
|
||||
presetExpectation.fulfill()
|
||||
}
|
||||
}
|
||||
let captureFormatMock = MockCaptureDeviceFormat()
|
||||
let captureDeviceMock = MockCaptureDevice()
|
||||
captureDeviceMock.flutterFormats = [captureFormatMock]
|
||||
var currentFormat: CaptureDeviceFormat = captureFormatMock
|
||||
captureDeviceMock.activeFormatStub = {
|
||||
return currentFormat
|
||||
}
|
||||
captureDeviceMock.lockForConfigurationStub = {
|
||||
lockForConfigurationExpectation.fulfill()
|
||||
}
|
||||
|
||||
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
||||
configuration.videoCaptureDeviceFactory = { _ in captureDeviceMock }
|
||||
configuration.videoDimensionsConverter = { _ in
|
||||
return CMVideoDimensions(width: 4, height: 3)
|
||||
}
|
||||
configuration.videoCaptureSession = videoSessionMock
|
||||
configuration.mediaSettings = CameraTestUtils.createDefaultMediaSettings(
|
||||
resolutionPreset: PlatformResolutionPreset.max)
|
||||
|
||||
let _ = CameraTestUtils.createTestCamera(configuration)
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testResolutionPresetWithCanSetSessionPresetMax_mustUpdateCaptureSessionPreset() {
|
||||
let expectedPreset = AVCaptureSession.Preset.hd4K3840x2160
|
||||
let expectation = self.expectation(description: "Expected preset set")
|
||||
|
||||
let videoSessionMock = MockCaptureSession()
|
||||
// Make sure that setting resolution preset for session always succeeds.
|
||||
videoSessionMock.canSetSessionPresetStub = { _ in true }
|
||||
videoSessionMock.setSessionPresetStub = { preset in
|
||||
if preset == expectedPreset {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
||||
configuration.videoCaptureSession = videoSessionMock
|
||||
configuration.mediaSettings = CameraTestUtils.createDefaultMediaSettings(
|
||||
resolutionPreset: PlatformResolutionPreset.max)
|
||||
configuration.videoCaptureDeviceFactory = { _ in MockCaptureDevice() }
|
||||
|
||||
let _ = CameraTestUtils.createTestCamera(configuration)
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testResolutionPresetWithCanSetSessionPresetUltraHigh_mustUpdateCaptureSessionPreset() {
|
||||
let expectedPreset = AVCaptureSession.Preset.hd4K3840x2160
|
||||
let expectation = self.expectation(description: "Expected preset set")
|
||||
|
||||
let videoSessionMock = MockCaptureSession()
|
||||
// Make sure that setting resolution preset for session always succeeds.
|
||||
videoSessionMock.canSetSessionPresetStub = { _ in true }
|
||||
// Expect that setting "ultraHigh" resolutionPreset correctly updates videoCaptureSession.
|
||||
videoSessionMock.setSessionPresetStub = { preset in
|
||||
if preset == expectedPreset {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
||||
configuration.videoCaptureSession = videoSessionMock
|
||||
configuration.mediaSettings = CameraTestUtils.createDefaultMediaSettings(
|
||||
resolutionPreset: PlatformResolutionPreset.ultraHigh)
|
||||
|
||||
let _ = CameraTestUtils.createTestCamera(configuration)
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class CameraSetDeviceOrientationTests: XCTestCase {
|
||||
private func createCamera() -> (Camera, MockCaptureConnection, MockCaptureConnection) {
|
||||
let camera = CameraTestUtils.createTestCamera()
|
||||
|
||||
let mockCapturePhotoOutput = MockCapturePhotoOutput()
|
||||
let mockPhotoCaptureConnection = MockCaptureConnection()
|
||||
mockPhotoCaptureConnection.isVideoOrientationSupported = true
|
||||
|
||||
mockCapturePhotoOutput.connectionWithMediaTypeStub = { _ in mockPhotoCaptureConnection }
|
||||
camera.capturePhotoOutput = mockCapturePhotoOutput
|
||||
|
||||
let mockCaptureVideoDataOutput = MockCaptureVideoDataOutput()
|
||||
let mockVideoCaptureConnection = MockCaptureConnection()
|
||||
mockVideoCaptureConnection.isVideoOrientationSupported = true
|
||||
|
||||
mockCaptureVideoDataOutput.connectionWithMediaTypeStub = { _ in mockVideoCaptureConnection }
|
||||
camera.captureVideoOutput = mockCaptureVideoDataOutput
|
||||
|
||||
return (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection)
|
||||
}
|
||||
|
||||
func testSetDeviceOrientation_setsOrientationsOfCaptureConnections() {
|
||||
let (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) = createCamera()
|
||||
var photoSetVideoOrientationCalled = false
|
||||
mockPhotoCaptureConnection.setVideoOrientationStub = { orientation in
|
||||
// Device orientation is flipped compared to video orientation. When UIDeviceOrientation
|
||||
// is landscape left the video orientation should be landscape right.
|
||||
XCTAssertEqual(orientation, .landscapeRight)
|
||||
photoSetVideoOrientationCalled = true
|
||||
}
|
||||
|
||||
var videoSetVideoOrientationCalled = false
|
||||
mockVideoCaptureConnection.setVideoOrientationStub = { orientation in
|
||||
// Device orientation is flipped compared to video orientation. When UIDeviceOrientation
|
||||
// is landscape left the video orientation should be landscape right.
|
||||
XCTAssertEqual(orientation, .landscapeRight)
|
||||
videoSetVideoOrientationCalled = true
|
||||
}
|
||||
|
||||
camera.deviceOrientation = .landscapeLeft
|
||||
|
||||
XCTAssertTrue(photoSetVideoOrientationCalled)
|
||||
XCTAssertTrue(videoSetVideoOrientationCalled)
|
||||
}
|
||||
|
||||
func
|
||||
testSetDeviceOrientation_setsLockedOrientationsOfCaptureConnection_ifCaptureOrientationIsLocked()
|
||||
{
|
||||
let (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) = createCamera()
|
||||
var photoSetVideoOrientationCalled = false
|
||||
mockPhotoCaptureConnection.setVideoOrientationStub = { orientation in
|
||||
XCTAssertEqual(orientation, .portraitUpsideDown)
|
||||
photoSetVideoOrientationCalled = true
|
||||
}
|
||||
|
||||
var videoSetVideoOrientationCalled = false
|
||||
mockVideoCaptureConnection.setVideoOrientationStub = { orientation in
|
||||
XCTAssertEqual(orientation, .portraitUpsideDown)
|
||||
videoSetVideoOrientationCalled = true
|
||||
}
|
||||
|
||||
camera.lockCaptureOrientation(PlatformDeviceOrientation.portraitDown)
|
||||
|
||||
camera.deviceOrientation = .landscapeLeft
|
||||
|
||||
XCTAssertTrue(photoSetVideoOrientationCalled)
|
||||
XCTAssertTrue(videoSetVideoOrientationCalled)
|
||||
}
|
||||
|
||||
func testSetDeviceOrientation_doesNotSetOrientations_ifRecordingIsInProgress() {
|
||||
let (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) = createCamera()
|
||||
|
||||
camera.startVideoRecording(completion: { _ in }, messengerForStreaming: nil)
|
||||
|
||||
mockPhotoCaptureConnection.setVideoOrientationStub = { _ in XCTFail() }
|
||||
mockVideoCaptureConnection.setVideoOrientationStub = { _ in XCTFail() }
|
||||
|
||||
camera.deviceOrientation = .landscapeLeft
|
||||
}
|
||||
|
||||
func testSetDeviceOrientation_doesNotSetOrientations_forDuplicateUpdates() {
|
||||
let (camera, mockPhotoCaptureConnection, mockVideoCaptureConnection) = createCamera()
|
||||
var photoSetVideoOrientationCallCount = 0
|
||||
mockPhotoCaptureConnection.setVideoOrientationStub = { _ in
|
||||
photoSetVideoOrientationCallCount += 1
|
||||
}
|
||||
|
||||
var videoSetVideoOrientationCallCount = 0
|
||||
mockVideoCaptureConnection.setVideoOrientationStub = { _ in
|
||||
videoSetVideoOrientationCallCount += 1
|
||||
}
|
||||
|
||||
camera.deviceOrientation = .landscapeRight
|
||||
camera.deviceOrientation = .landscapeRight
|
||||
|
||||
XCTAssertEqual(photoSetVideoOrientationCallCount, 1)
|
||||
XCTAssertEqual(videoSetVideoOrientationCallCount, 1)
|
||||
}
|
||||
}
|
||||
168
camera_avfoundation/darwin/Tests/CameraSetFlashModeTests.swift
Normal file
168
camera_avfoundation/darwin/Tests/CameraSetFlashModeTests.swift
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class CameraSetFlashModeTests: XCTestCase {
|
||||
private func createCamera() -> (Camera, MockCaptureDevice, MockCapturePhotoOutput) {
|
||||
let mockDevice = MockCaptureDevice()
|
||||
let mockCapturePhotoOutput = MockCapturePhotoOutput()
|
||||
|
||||
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
||||
configuration.videoCaptureDeviceFactory = { _ in mockDevice }
|
||||
let camera = CameraTestUtils.createTestCamera(configuration)
|
||||
camera.capturePhotoOutput = mockCapturePhotoOutput
|
||||
|
||||
return (camera, mockDevice, mockCapturePhotoOutput)
|
||||
}
|
||||
|
||||
func testSetFlashModeWithTorchMode_setsTrochModeOn() {
|
||||
let (camera, mockDevice, _) = createCamera()
|
||||
|
||||
mockDevice.hasTorch = true
|
||||
mockDevice.isTorchAvailable = true
|
||||
|
||||
var setTorchModeCalled = false
|
||||
mockDevice.setTorchModeStub = { torchMode in
|
||||
XCTAssertEqual(torchMode, .on)
|
||||
setTorchModeCalled = true
|
||||
}
|
||||
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
camera.setFlashMode(.torch) {
|
||||
result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30)
|
||||
|
||||
XCTAssertTrue(setTorchModeCalled)
|
||||
}
|
||||
|
||||
func testSetFlashModeWithTorchMode_returnError_ifHasNoTorch() {
|
||||
let (camera, mockDevice, _) = createCamera()
|
||||
|
||||
mockDevice.hasTorch = false
|
||||
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
camera.setFlashMode(.torch) { result in
|
||||
switch result {
|
||||
case .failure(let error as PigeonError):
|
||||
XCTAssertEqual(error.code, "setFlashModeFailed")
|
||||
XCTAssertEqual(error.message, "Device does not support torch mode")
|
||||
default:
|
||||
XCTFail("Expected failure")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30)
|
||||
}
|
||||
|
||||
func testSetFlashModeWithTorchMode_returnError_ifTorchIsNotAvailable() {
|
||||
let (camera, mockDevice, _) = createCamera()
|
||||
|
||||
mockDevice.hasTorch = true
|
||||
mockDevice.isTorchAvailable = false
|
||||
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
camera.setFlashMode(.torch) { result in
|
||||
switch result {
|
||||
case .failure(let error as PigeonError):
|
||||
XCTAssertEqual(error.code, "setFlashModeFailed")
|
||||
XCTAssertEqual(error.message, "Torch mode is currently not available")
|
||||
default:
|
||||
XCTFail("Expected failure")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30)
|
||||
}
|
||||
|
||||
func testSetFlashModeWithNonTorchMode_setsTrochModeOff_ifTorchModeIsEnabled() {
|
||||
let (camera, mockDevice, mockCapturePhotoOutput) = createCamera()
|
||||
|
||||
mockCapturePhotoOutput.supportedFlashModes = [.auto]
|
||||
|
||||
mockDevice.hasFlash = true
|
||||
// Torch mode is enabled
|
||||
mockDevice.getTorchModeStub = { .on }
|
||||
|
||||
var setTorchModeCalled = false
|
||||
mockDevice.setTorchModeStub = { torchMode in
|
||||
XCTAssertEqual(torchMode, .off)
|
||||
setTorchModeCalled = true
|
||||
}
|
||||
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
camera.setFlashMode(.auto) {
|
||||
result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure:
|
||||
XCTFail("Unexpected failure")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30)
|
||||
|
||||
XCTAssertTrue(setTorchModeCalled)
|
||||
}
|
||||
|
||||
func testSetFlashModeWithNonTorchMode_returnError_ifHasNoFlash() {
|
||||
let (camera, mockDevice, _) = createCamera()
|
||||
|
||||
mockDevice.hasFlash = false
|
||||
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
camera.setFlashMode(.auto) { result in
|
||||
switch result {
|
||||
case .failure(let error as PigeonError):
|
||||
XCTAssertEqual(error.code, "setFlashModeFailed")
|
||||
XCTAssertEqual(error.message, "Device does not have flash capabilities")
|
||||
default:
|
||||
XCTFail("Expected failure")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30)
|
||||
}
|
||||
|
||||
func testSetFlashModeWithNonTorchMode_returnError_ifModeIsNotSupported() {
|
||||
let (camera, mockDevice, mockCapturePhotoOutput) = createCamera()
|
||||
|
||||
// No flash modes are supported
|
||||
mockCapturePhotoOutput.supportedFlashModes = []
|
||||
|
||||
mockDevice.hasFlash = true
|
||||
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
camera.setFlashMode(.auto) { result in
|
||||
switch result {
|
||||
case .failure(let error as PigeonError):
|
||||
XCTAssertEqual(error.code, "setFlashModeFailed")
|
||||
XCTAssertEqual(error.message, "Device does not support this specific flash mode")
|
||||
default:
|
||||
XCTFail("Expected failure")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30)
|
||||
}
|
||||
}
|
||||
166
camera_avfoundation/darwin/Tests/CameraSetFocusModeTests.swift
Normal file
166
camera_avfoundation/darwin/Tests/CameraSetFocusModeTests.swift
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class CameraSetFocusModeTests: XCTestCase {
|
||||
private func createCamera() -> (Camera, MockCaptureDevice, MockDeviceOrientationProvider) {
|
||||
let mockDevice = MockCaptureDevice()
|
||||
let mockDeviceOrientationProvider = MockDeviceOrientationProvider()
|
||||
|
||||
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
||||
configuration.videoCaptureDeviceFactory = { _ in mockDevice }
|
||||
configuration.deviceOrientationProvider = mockDeviceOrientationProvider
|
||||
let camera = CameraTestUtils.createTestCamera(configuration)
|
||||
|
||||
return (camera, mockDevice, mockDeviceOrientationProvider)
|
||||
}
|
||||
|
||||
func testAutoFocusWithContinuousModeSupported_ShouldSetContinuousAutoFocus() {
|
||||
let (camera, mockDevice, _) = createCamera()
|
||||
// AVCaptureFocusModeContinuousAutoFocus and AVCaptureFocusModeAutoFocus are supported.
|
||||
mockDevice.isFocusModeSupportedStub = { mode in
|
||||
mode == .continuousAutoFocus || mode == .autoFocus
|
||||
}
|
||||
|
||||
var setFocusModeContinuousAutoFocusCalled = false
|
||||
|
||||
mockDevice.setFocusModeStub = { mode in
|
||||
// Don't expect setFocusMode:AVCaptureFocusModeAutoFocus.
|
||||
if mode == .autoFocus {
|
||||
XCTFail("Unexpected call to setFocusMode")
|
||||
} else if mode == .continuousAutoFocus {
|
||||
setFocusModeContinuousAutoFocusCalled = true
|
||||
}
|
||||
}
|
||||
|
||||
camera.setFocusMode(.auto)
|
||||
|
||||
XCTAssertTrue(setFocusModeContinuousAutoFocusCalled)
|
||||
}
|
||||
|
||||
func testAutoFocusWithContinuousModeNotSupported_ShouldSetAutoFocus() {
|
||||
let (camera, mockDevice, _) = createCamera()
|
||||
// AVCaptureFocusModeContinuousAutoFocus is not supported.
|
||||
// AVCaptureFocusModeAutoFocus is supported.
|
||||
mockDevice.isFocusModeSupportedStub = { mode in
|
||||
mode == .autoFocus
|
||||
}
|
||||
|
||||
var setFocusModeAutoFocusCalled = false
|
||||
|
||||
// Don't expect setFocusMode:AVCaptureFocusModeContinuousAutoFocus.
|
||||
mockDevice.setFocusModeStub = { mode in
|
||||
if mode == .continuousAutoFocus {
|
||||
XCTFail("Unexpected call to setFocusMode")
|
||||
} else if mode == .autoFocus {
|
||||
setFocusModeAutoFocusCalled = true
|
||||
}
|
||||
}
|
||||
|
||||
camera.setFocusMode(.auto)
|
||||
|
||||
XCTAssertTrue(setFocusModeAutoFocusCalled)
|
||||
}
|
||||
|
||||
func testAutoFocusWithNoModeSupported_ShouldSetNothing() {
|
||||
let (camera, mockDevice, _) = createCamera()
|
||||
// No modes are supported.
|
||||
mockDevice.isFocusModeSupportedStub = { _ in
|
||||
false
|
||||
}
|
||||
|
||||
// Don't expect any setFocus.
|
||||
mockDevice.setFocusModeStub = {
|
||||
_ in XCTFail("Unexpected call to setFocusMode")
|
||||
}
|
||||
|
||||
camera.setFocusMode(.auto)
|
||||
}
|
||||
|
||||
func testLockedFocusWithModeSupported_ShouldSetModeAutoFocus() {
|
||||
let (camera, mockDevice, _) = createCamera()
|
||||
// AVCaptureFocusModeContinuousAutoFocus and AVCaptureFocusModeAutoFocus are supported.
|
||||
mockDevice.isFocusModeSupportedStub = { mode in
|
||||
mode == .continuousAutoFocus || mode == .autoFocus
|
||||
}
|
||||
|
||||
var setFocusModeAutoFocusCalled = false
|
||||
|
||||
// AVCaptureFocusModeAutoFocus automatically adjusts the focus one time, and then locks focus
|
||||
mockDevice.setFocusModeStub = { mode in
|
||||
if mode == .continuousAutoFocus {
|
||||
XCTFail("Unexpected call to setFocusMode")
|
||||
} else if mode == .autoFocus {
|
||||
setFocusModeAutoFocusCalled = true
|
||||
}
|
||||
}
|
||||
|
||||
camera.setFocusMode(.locked)
|
||||
|
||||
XCTAssertTrue(setFocusModeAutoFocusCalled)
|
||||
}
|
||||
|
||||
func testLockedFocusWithModeNotSupported_ShouldSetNothing() {
|
||||
let (camera, mockDevice, _) = createCamera()
|
||||
mockDevice.isFocusModeSupportedStub = { mode in
|
||||
mode == .continuousAutoFocus
|
||||
}
|
||||
|
||||
// Don't expect any setFocus.
|
||||
mockDevice.setFocusModeStub = { _ in
|
||||
XCTFail("Unexpected call to setFocusMode")
|
||||
}
|
||||
|
||||
camera.setFocusMode(.locked)
|
||||
}
|
||||
|
||||
func testSetFocusPointWithResult_SetsFocusPointOfInterest() {
|
||||
let (camera, mockDevice, mockDeviceOrientationProvider) = createCamera()
|
||||
// UI is currently in landscape left orientation.
|
||||
mockDeviceOrientationProvider.orientationStub = { .landscapeLeft }
|
||||
// Focus point of interest is supported.
|
||||
mockDevice.isFocusPointOfInterestSupported = true
|
||||
|
||||
var setFocusPointOfInterestCalled = false
|
||||
mockDevice.setFocusPointOfInterestStub = { point in
|
||||
if point == CGPoint(x: 1.0, y: 1.0) {
|
||||
setFocusPointOfInterestCalled = true
|
||||
}
|
||||
}
|
||||
|
||||
camera.setFocusPoint(PlatformPoint(x: 1, y: 1)) {
|
||||
result in
|
||||
let _ = self.assertSuccess(result)
|
||||
}
|
||||
|
||||
XCTAssertTrue(setFocusPointOfInterestCalled)
|
||||
}
|
||||
|
||||
func testSetFocusPoint_WhenNotSupported_ReturnsError() {
|
||||
let (camera, mockDevice, mockDeviceOrientationProvider) = createCamera()
|
||||
// UI is currently in landscape left orientation.
|
||||
mockDeviceOrientationProvider.orientationStub = { .landscapeLeft }
|
||||
// Focus point of interest is not supported.
|
||||
mockDevice.isFocusPointOfInterestSupported = false
|
||||
|
||||
let expectation = self.expectation(description: "Completion with error")
|
||||
|
||||
camera.setFocusPoint(PlatformPoint(x: 1, y: 1)) { result in
|
||||
switch result {
|
||||
case .failure(let error as PigeonError):
|
||||
XCTAssertEqual(error.code, "setFocusPointFailed")
|
||||
XCTAssertEqual(error.message, "Device does not have focus point capabilities")
|
||||
default:
|
||||
XCTFail("Expected failure")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
}
|
||||
356
camera_avfoundation/darwin/Tests/CameraSettingsTests.swift
Normal file
356
camera_avfoundation/darwin/Tests/CameraSettingsTests.swift
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import Flutter
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
private let testResolutionPreset = PlatformResolutionPreset.medium
|
||||
private let testFramesPerSecond: Int64 = 15
|
||||
private let testVideoBitrate: Int64 = 200000
|
||||
private let testAudioBitrate: Int64 = 32000
|
||||
|
||||
private final class TestMediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper {
|
||||
let lockExpectation: XCTestExpectation
|
||||
let unlockExpectation: XCTestExpectation
|
||||
let minFrameDurationExpectation: XCTestExpectation
|
||||
let maxFrameDurationExpectation: XCTestExpectation
|
||||
let beginConfigurationExpectation: XCTestExpectation
|
||||
let commitConfigurationExpectation: XCTestExpectation
|
||||
let audioSettingsExpectation: XCTestExpectation
|
||||
let videoSettingsExpectation: XCTestExpectation
|
||||
|
||||
init(test: XCTestCase, expectAudio: Bool) {
|
||||
lockExpectation = test.expectation(description: "lockExpectation")
|
||||
unlockExpectation = test.expectation(description: "unlockExpectation")
|
||||
minFrameDurationExpectation = test.expectation(description: "minFrameDurationExpectation")
|
||||
maxFrameDurationExpectation = test.expectation(description: "maxFrameDurationExpectation")
|
||||
beginConfigurationExpectation = test.expectation(description: "beginConfigurationExpectation")
|
||||
commitConfigurationExpectation = test.expectation(description: "commitConfigurationExpectation")
|
||||
audioSettingsExpectation = test.expectation(description: "audioSettingsExpectation")
|
||||
audioSettingsExpectation.isInverted = !expectAudio
|
||||
videoSettingsExpectation = test.expectation(description: "videoSettingsExpectation")
|
||||
}
|
||||
|
||||
override func lockDevice(_ captureDevice: CaptureDevice) throws {
|
||||
lockExpectation.fulfill()
|
||||
}
|
||||
|
||||
override func unlockDevice(_ captureDevice: CaptureDevice) {
|
||||
unlockExpectation.fulfill()
|
||||
}
|
||||
|
||||
override func beginConfiguration(for videoCaptureSession: CaptureSession) {
|
||||
beginConfigurationExpectation.fulfill()
|
||||
}
|
||||
|
||||
override func commitConfiguration(for videoCaptureSession: CaptureSession) {
|
||||
commitConfigurationExpectation.fulfill()
|
||||
}
|
||||
|
||||
override func setMinFrameDuration(_ duration: CMTime, on captureDevice: CaptureDevice) {
|
||||
// Camera allows to set frame rate with 1/10 precision.
|
||||
let expectedDuration = CMTimeMake(value: 10, timescale: Int32(testFramesPerSecond * 10))
|
||||
if duration == expectedDuration {
|
||||
minFrameDurationExpectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
override func setMaxFrameDuration(_ duration: CMTime, on captureDevice: CaptureDevice) {
|
||||
// Camera allows to set frame rate with 1/10 precision.
|
||||
let expectedDuration = CMTimeMake(value: 10, timescale: Int32(testFramesPerSecond * 10))
|
||||
if duration == expectedDuration {
|
||||
maxFrameDurationExpectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
override func assetWriterAudioInput(withOutputSettings outputSettings: [String: Any]?)
|
||||
-> AssetWriterInput
|
||||
{
|
||||
if let bitrate = outputSettings?[AVEncoderBitRateKey] as? Int, bitrate == Int(testAudioBitrate)
|
||||
{
|
||||
audioSettingsExpectation.fulfill()
|
||||
}
|
||||
return MockAssetWriterInput()
|
||||
}
|
||||
|
||||
override func assetWriterVideoInput(withOutputSettings outputSettings: [String: Any]?)
|
||||
-> AssetWriterInput
|
||||
{
|
||||
if let compressionProperties = outputSettings?[AVVideoCompressionPropertiesKey]
|
||||
as? [String: Any],
|
||||
let bitrate = compressionProperties[AVVideoAverageBitRateKey] as? Int,
|
||||
let frameRate = compressionProperties[AVVideoExpectedSourceFrameRateKey] as? Double,
|
||||
bitrate == testVideoBitrate, frameRate == Double(testFramesPerSecond)
|
||||
{
|
||||
videoSettingsExpectation.fulfill()
|
||||
}
|
||||
|
||||
// AVAssetWriterInput needs these three keys, otherwise it throws.
|
||||
var outputSettingsWithRequiredKeys = outputSettings ?? [:]
|
||||
outputSettingsWithRequiredKeys[AVVideoCodecKey] = AVVideoCodecType.h264
|
||||
outputSettingsWithRequiredKeys[AVVideoWidthKey] = 1280
|
||||
outputSettingsWithRequiredKeys[AVVideoHeightKey] = 720
|
||||
|
||||
return MockAssetWriterInput()
|
||||
}
|
||||
|
||||
override func addInput(_ writerInput: AssetWriterInput, to writer: AssetWriter) {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
override func recommendedVideoSettingsForAssetWriter(
|
||||
withFileType fileType: AVFileType, for output: CaptureVideoDataOutput
|
||||
) -> [String: Any]? {
|
||||
return [:]
|
||||
}
|
||||
}
|
||||
|
||||
final class CameraSettingsTests: XCTestCase {
|
||||
func testSettings_shouldPassConfigurationToCameraDeviceAndWriter() {
|
||||
let enableAudio: Bool = true
|
||||
let settings = PlatformMediaSettings(
|
||||
resolutionPreset: testResolutionPreset,
|
||||
framesPerSecond: testFramesPerSecond,
|
||||
videoBitrate: testVideoBitrate,
|
||||
audioBitrate: testAudioBitrate,
|
||||
enableAudio: enableAudio
|
||||
)
|
||||
let injectedWrapper = TestMediaSettingsAVWrapper(test: self, expectAudio: enableAudio)
|
||||
|
||||
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
||||
configuration.mediaSettingsWrapper = injectedWrapper
|
||||
configuration.mediaSettings = settings
|
||||
let camera = CameraTestUtils.createTestCamera(configuration)
|
||||
|
||||
// Expect FPS configuration is passed to camera device.
|
||||
wait(
|
||||
for: [
|
||||
injectedWrapper.lockExpectation,
|
||||
injectedWrapper.beginConfigurationExpectation,
|
||||
injectedWrapper.minFrameDurationExpectation,
|
||||
injectedWrapper.maxFrameDurationExpectation,
|
||||
injectedWrapper.commitConfigurationExpectation,
|
||||
injectedWrapper.unlockExpectation,
|
||||
], timeout: 1, enforceOrder: true)
|
||||
|
||||
camera.startVideoRecording(
|
||||
completion: { error in
|
||||
// No-op.
|
||||
}, messengerForStreaming: nil)
|
||||
|
||||
wait(
|
||||
for: [
|
||||
injectedWrapper.audioSettingsExpectation,
|
||||
injectedWrapper.videoSettingsExpectation,
|
||||
], timeout: 1)
|
||||
}
|
||||
|
||||
func testSettings_ShouldBeSupportedByMethodCall() {
|
||||
let mockDevice = MockCaptureDevice()
|
||||
let mockSession = MockCaptureSession()
|
||||
mockSession.canSetSessionPresetStub = { _ in true }
|
||||
let camera = CameraPlugin(
|
||||
registry: MockFlutterTextureRegistry(),
|
||||
messenger: MockFlutterBinaryMessenger(),
|
||||
globalAPI: MockGlobalEventApi(),
|
||||
deviceDiscoverer: MockCameraDeviceDiscoverer(),
|
||||
permissionManager: MockCameraPermissionManager(),
|
||||
deviceFactory: { _ in mockDevice },
|
||||
captureSessionFactory: { mockSession },
|
||||
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
|
||||
captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue")
|
||||
)
|
||||
|
||||
let expectation = self.expectation(description: "Result finished")
|
||||
let mediaSettings = PlatformMediaSettings(
|
||||
resolutionPreset: testResolutionPreset,
|
||||
framesPerSecond: testFramesPerSecond,
|
||||
videoBitrate: testVideoBitrate,
|
||||
audioBitrate: testAudioBitrate,
|
||||
enableAudio: false
|
||||
)
|
||||
var resultValue: Int64?
|
||||
camera.createCameraOnSessionQueue(
|
||||
withName: "acamera",
|
||||
settings: mediaSettings
|
||||
) { result in
|
||||
resultValue = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
XCTAssertNotNil(resultValue)
|
||||
}
|
||||
|
||||
func testSettings_ShouldSelectFormatWhichSupports60FPS() {
|
||||
let settings = PlatformMediaSettings(
|
||||
resolutionPreset: testResolutionPreset,
|
||||
framesPerSecond: 60,
|
||||
videoBitrate: testVideoBitrate,
|
||||
audioBitrate: testAudioBitrate,
|
||||
enableAudio: false
|
||||
)
|
||||
|
||||
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
||||
configuration.mediaSettings = settings
|
||||
let camera = CameraTestUtils.createTestCamera(configuration)
|
||||
|
||||
let range = camera.captureDevice.flutterActiveFormat.flutterVideoSupportedFrameRateRanges[0]
|
||||
XCTAssertLessThanOrEqual(range.minFrameRate, 60)
|
||||
XCTAssertGreaterThanOrEqual(range.maxFrameRate, 60)
|
||||
}
|
||||
|
||||
func test_setUpCaptureSessionForAudioIfNeeded_skipsAudioSession_whenAudioDisabled() {
|
||||
let settings = PlatformMediaSettings(
|
||||
resolutionPreset: testResolutionPreset,
|
||||
framesPerSecond: testFramesPerSecond,
|
||||
videoBitrate: testVideoBitrate,
|
||||
audioBitrate: testAudioBitrate,
|
||||
enableAudio: false
|
||||
)
|
||||
|
||||
let wrapper = TestMediaSettingsAVWrapper(test: self, expectAudio: false)
|
||||
let mockAudioSession = MockCaptureSession()
|
||||
|
||||
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
||||
configuration.mediaSettingsWrapper = wrapper
|
||||
configuration.mediaSettings = settings
|
||||
configuration.audioCaptureSession = mockAudioSession
|
||||
let camera = CameraTestUtils.createTestCamera(configuration)
|
||||
|
||||
wait(
|
||||
for: [
|
||||
wrapper.lockExpectation,
|
||||
wrapper.beginConfigurationExpectation,
|
||||
wrapper.minFrameDurationExpectation,
|
||||
wrapper.maxFrameDurationExpectation,
|
||||
wrapper.commitConfigurationExpectation,
|
||||
wrapper.unlockExpectation,
|
||||
],
|
||||
timeout: 1,
|
||||
enforceOrder: true
|
||||
)
|
||||
|
||||
camera.startVideoRecording(completion: { _ in }, messengerForStreaming: nil)
|
||||
|
||||
wait(
|
||||
for: [
|
||||
wrapper.audioSettingsExpectation,
|
||||
wrapper.videoSettingsExpectation,
|
||||
],
|
||||
timeout: 1
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
mockAudioSession.addedAudioOutputCount, 0,
|
||||
"Audio session should not receive AVCaptureAudioDataOutput when enableAudio is false"
|
||||
)
|
||||
}
|
||||
|
||||
func test_setUpCaptureSessionForAudioIfNeeded_addsAudioSession_whenAudioEnabled() {
|
||||
let settings = PlatformMediaSettings(
|
||||
resolutionPreset: testResolutionPreset,
|
||||
framesPerSecond: testFramesPerSecond,
|
||||
videoBitrate: testVideoBitrate,
|
||||
audioBitrate: testAudioBitrate,
|
||||
enableAudio: true
|
||||
)
|
||||
|
||||
let wrapper = TestMediaSettingsAVWrapper(test: self, expectAudio: true)
|
||||
let mockAudioSession = MockCaptureSession()
|
||||
|
||||
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
||||
configuration.mediaSettingsWrapper = wrapper
|
||||
configuration.mediaSettings = settings
|
||||
configuration.audioCaptureSession = mockAudioSession
|
||||
let camera = CameraTestUtils.createTestCamera(configuration)
|
||||
|
||||
wait(
|
||||
for: [
|
||||
wrapper.lockExpectation,
|
||||
wrapper.beginConfigurationExpectation,
|
||||
wrapper.minFrameDurationExpectation,
|
||||
wrapper.maxFrameDurationExpectation,
|
||||
wrapper.commitConfigurationExpectation,
|
||||
wrapper.unlockExpectation,
|
||||
],
|
||||
timeout: 1,
|
||||
enforceOrder: true
|
||||
)
|
||||
|
||||
camera.startVideoRecording(completion: { _ in }, messengerForStreaming: nil)
|
||||
|
||||
wait(
|
||||
for: [
|
||||
wrapper.audioSettingsExpectation,
|
||||
wrapper.videoSettingsExpectation,
|
||||
],
|
||||
timeout: 1
|
||||
)
|
||||
|
||||
XCTAssertGreaterThan(
|
||||
mockAudioSession.addedAudioOutputCount, 0,
|
||||
"Audio session should receive AVCaptureAudioDataOutput when enableAudio is true"
|
||||
)
|
||||
}
|
||||
|
||||
func testResolutionPresetWithMax_mustIgnoreLossyFormatsAndSquares() {
|
||||
let videoSessionMock = MockCaptureSession()
|
||||
videoSessionMock.canSetSessionPresetStub = { _ in true }
|
||||
|
||||
let lossyFormat = MockCaptureDeviceFormat(
|
||||
codecType: 1_651_798_066, // 'btp2'
|
||||
width: 4224,
|
||||
height: 3024
|
||||
)
|
||||
let squareFormat = MockCaptureDeviceFormat(
|
||||
codecType: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
|
||||
width: 4032,
|
||||
height: 4032
|
||||
)
|
||||
let safe4KFormat = MockCaptureDeviceFormat(
|
||||
codecType: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
|
||||
width: 3840,
|
||||
height: 2160
|
||||
)
|
||||
|
||||
let captureDeviceMock = MockCaptureDevice()
|
||||
captureDeviceMock.flutterFormats = [lossyFormat, squareFormat, safe4KFormat]
|
||||
|
||||
var currentFormat: CaptureDeviceFormat = safe4KFormat
|
||||
captureDeviceMock.activeFormatStub = { currentFormat }
|
||||
captureDeviceMock.setActiveFormatStub = { newFormat in currentFormat = newFormat }
|
||||
|
||||
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
||||
configuration.videoCaptureDeviceFactory = { _ in captureDeviceMock }
|
||||
configuration.videoCaptureSession = videoSessionMock
|
||||
|
||||
configuration.videoDimensionsConverter = { format in
|
||||
return CMVideoFormatDescriptionGetDimensions(format.formatDescription)
|
||||
}
|
||||
|
||||
configuration.mediaSettings = CameraTestUtils.createDefaultMediaSettings(
|
||||
resolutionPreset: PlatformResolutionPreset.max
|
||||
)
|
||||
|
||||
let _ = CameraTestUtils.createTestCamera(configuration)
|
||||
|
||||
let selectedFormat = captureDeviceMock.flutterActiveFormat
|
||||
let selectedDimensions = CMVideoFormatDescriptionGetDimensions(selectedFormat.formatDescription)
|
||||
|
||||
XCTAssertEqual(
|
||||
selectedDimensions.width,
|
||||
3840,
|
||||
"Camera should have ignored the lossy and square formats, safely falling back to 4K."
|
||||
)
|
||||
XCTAssertEqual(
|
||||
selectedDimensions.height,
|
||||
2160,
|
||||
"Camera should have ignored the lossy and square formats, safely falling back to 4K."
|
||||
)
|
||||
}
|
||||
}
|
||||
219
camera_avfoundation/darwin/Tests/CameraTestUtils.swift
Normal file
219
camera_avfoundation/darwin/Tests/CameraTestUtils.swift
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// Utils for creating default class instances used in tests
|
||||
enum CameraTestUtils {
|
||||
/// This method provides a convenient way to create media settings with minimal configuration.
|
||||
/// Audio is enabled by default, while other parameters use platform-specific defaults.
|
||||
static func createDefaultMediaSettings(resolutionPreset: PlatformResolutionPreset)
|
||||
-> PlatformMediaSettings
|
||||
{
|
||||
return PlatformMediaSettings(
|
||||
resolutionPreset: resolutionPreset,
|
||||
framesPerSecond: nil,
|
||||
videoBitrate: nil,
|
||||
audioBitrate: nil,
|
||||
enableAudio: true)
|
||||
}
|
||||
|
||||
/// Creates a test `CameraConfiguration` with a default mock setup.
|
||||
static func createTestCameraConfiguration() -> CameraConfiguration {
|
||||
let captureSessionQueue = DispatchQueue(label: "capture_session_queue")
|
||||
|
||||
let videoSessionMock = MockCaptureSession()
|
||||
videoSessionMock.canSetSessionPresetStub = { _ in true }
|
||||
|
||||
let audioSessionMock = MockCaptureSession()
|
||||
audioSessionMock.canSetSessionPresetStub = { _ in true }
|
||||
|
||||
let frameRateRangeMock1 = MockFrameRateRange.init(minFrameRate: 3, maxFrameRate: 30)
|
||||
|
||||
let captureDeviceFormatMock1 = MockCaptureDeviceFormat()
|
||||
captureDeviceFormatMock1.flutterVideoSupportedFrameRateRanges = [frameRateRangeMock1]
|
||||
|
||||
let frameRateRangeMock2 = MockFrameRateRange.init(minFrameRate: 3, maxFrameRate: 60)
|
||||
|
||||
let captureDeviceFormatMock2 = MockCaptureDeviceFormat()
|
||||
captureDeviceFormatMock2.flutterVideoSupportedFrameRateRanges = [frameRateRangeMock2]
|
||||
|
||||
let captureDeviceMock = MockCaptureDevice()
|
||||
captureDeviceMock.flutterFormats = [captureDeviceFormatMock1, captureDeviceFormatMock2]
|
||||
|
||||
var currentFormat: CaptureDeviceFormat = captureDeviceFormatMock1
|
||||
|
||||
captureDeviceMock.activeFormatStub = { currentFormat }
|
||||
captureDeviceMock.setActiveFormatStub = { format in
|
||||
currentFormat = format
|
||||
}
|
||||
|
||||
let configuration = CameraConfiguration(
|
||||
mediaSettings: createDefaultMediaSettings(
|
||||
resolutionPreset: PlatformResolutionPreset.medium),
|
||||
mediaSettingsWrapper: FLTCamMediaSettingsAVWrapper(),
|
||||
captureDeviceFactory: { _ in captureDeviceMock },
|
||||
audioCaptureDeviceFactory: { MockCaptureDevice() },
|
||||
captureSessionFactory: { videoSessionMock },
|
||||
captureSessionQueue: captureSessionQueue,
|
||||
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
|
||||
initialCameraName: "camera_name"
|
||||
)
|
||||
|
||||
configuration.videoCaptureSession = videoSessionMock
|
||||
configuration.audioCaptureSession = audioSessionMock
|
||||
configuration.orientation = .portrait
|
||||
|
||||
configuration.assetWriterFactory = { _, _ in MockAssetWriter() }
|
||||
|
||||
configuration.inputPixelBufferAdaptorFactory = { _, _ in
|
||||
MockAssetWriterInputPixelBufferAdaptor()
|
||||
}
|
||||
|
||||
return configuration
|
||||
}
|
||||
|
||||
static func createTestCamera(_ configuration: CameraConfiguration) -> DefaultCamera {
|
||||
return (try? DefaultCamera(configuration: configuration))!
|
||||
}
|
||||
|
||||
static func createTestCamera() -> DefaultCamera {
|
||||
return createTestCamera(createTestCameraConfiguration())
|
||||
}
|
||||
|
||||
static func createCameraWithCaptureSessionQueue(_ captureSessionQueue: DispatchQueue)
|
||||
-> DefaultCamera
|
||||
{
|
||||
let configuration = createTestCameraConfiguration()
|
||||
configuration.captureSessionQueue = captureSessionQueue
|
||||
return createTestCamera(configuration)
|
||||
}
|
||||
|
||||
/// Creates a test sample buffer.
|
||||
/// @return a test sample buffer.
|
||||
static func createTestSampleBuffer(
|
||||
timestamp: CMTime = .zero,
|
||||
duration: CMTime = CMTimeMake(value: 1, timescale: 44100)
|
||||
) -> CMSampleBuffer {
|
||||
var pixelBuffer: CVPixelBuffer?
|
||||
CVPixelBufferCreate(kCFAllocatorDefault, 100, 100, kCVPixelFormatType_32BGRA, nil, &pixelBuffer)
|
||||
|
||||
var formatDescription: CMFormatDescription?
|
||||
CMVideoFormatDescriptionCreateForImageBuffer(
|
||||
allocator: kCFAllocatorDefault,
|
||||
imageBuffer: pixelBuffer!,
|
||||
formatDescriptionOut: &formatDescription)
|
||||
|
||||
var timingInfo = CMSampleTimingInfo(
|
||||
duration: duration,
|
||||
presentationTimeStamp: timestamp,
|
||||
decodeTimeStamp: .invalid)
|
||||
|
||||
var sampleBuffer: CMSampleBuffer?
|
||||
CMSampleBufferCreateReadyWithImageBuffer(
|
||||
allocator: kCFAllocatorDefault,
|
||||
imageBuffer: pixelBuffer!,
|
||||
formatDescription: formatDescription!,
|
||||
sampleTiming: &timingInfo,
|
||||
sampleBufferOut: &sampleBuffer)
|
||||
|
||||
return sampleBuffer!
|
||||
}
|
||||
|
||||
/// Creates a test audio sample buffer.
|
||||
/// @return a test audio sample buffer.
|
||||
static func createTestAudioSampleBuffer(
|
||||
timestamp: CMTime = .zero,
|
||||
duration: CMTime = CMTimeMake(value: 1, timescale: 44100)
|
||||
) -> CMSampleBuffer {
|
||||
var blockBuffer: CMBlockBuffer?
|
||||
CMBlockBufferCreateWithMemoryBlock(
|
||||
allocator: kCFAllocatorDefault,
|
||||
memoryBlock: nil,
|
||||
blockLength: Int(duration.value),
|
||||
blockAllocator: kCFAllocatorDefault,
|
||||
customBlockSource: nil,
|
||||
offsetToData: 0,
|
||||
dataLength: Int(duration.value),
|
||||
flags: kCMBlockBufferAssureMemoryNowFlag,
|
||||
blockBufferOut: &blockBuffer)
|
||||
|
||||
var formatDescription: CMFormatDescription?
|
||||
var basicDescription = AudioStreamBasicDescription(
|
||||
mSampleRate: Float64(duration.timescale),
|
||||
mFormatID: kAudioFormatLinearPCM,
|
||||
mFormatFlags: 0,
|
||||
mBytesPerPacket: 1,
|
||||
mFramesPerPacket: 1,
|
||||
mBytesPerFrame: 1,
|
||||
mChannelsPerFrame: 1,
|
||||
mBitsPerChannel: 8,
|
||||
mReserved: 0)
|
||||
|
||||
CMAudioFormatDescriptionCreate(
|
||||
allocator: kCFAllocatorDefault,
|
||||
asbd: &basicDescription,
|
||||
layoutSize: 0,
|
||||
layout: nil,
|
||||
magicCookieSize: 0,
|
||||
magicCookie: nil,
|
||||
extensions: nil,
|
||||
formatDescriptionOut: &formatDescription)
|
||||
|
||||
var sampleBuffer: CMSampleBuffer?
|
||||
CMAudioSampleBufferCreateReadyWithPacketDescriptions(
|
||||
allocator: kCFAllocatorDefault,
|
||||
dataBuffer: blockBuffer!,
|
||||
formatDescription: formatDescription!,
|
||||
sampleCount: CMItemCount(duration.value),
|
||||
presentationTimeStamp: timestamp,
|
||||
packetDescriptions: nil,
|
||||
sampleBufferOut: &sampleBuffer)
|
||||
|
||||
return sampleBuffer!
|
||||
}
|
||||
|
||||
static func createTestAudioOutput() -> AVCaptureOutput {
|
||||
return AVCaptureAudioDataOutput()
|
||||
}
|
||||
|
||||
static func createTestConnection(_ output: AVCaptureOutput) -> AVCaptureConnection {
|
||||
return AVCaptureConnection(inputPorts: [], output: output)
|
||||
}
|
||||
}
|
||||
|
||||
extension XCTestCase {
|
||||
/// Wait until a round trip of a given `DispatchQueue` is complete. This allows for testing
|
||||
/// side-effects of async functions that do not provide any notification of completion.
|
||||
func waitForQueueRoundTrip(with queue: DispatchQueue) {
|
||||
let expectation = expectation(description: "Queue flush")
|
||||
|
||||
queue.async {
|
||||
if queue == DispatchQueue.main {
|
||||
expectation.fulfill()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wait(for: [expectation], timeout: 1)
|
||||
}
|
||||
|
||||
func assertSuccess<T>(
|
||||
_ result: Result<T, any Error>, file: StaticString = #file, line: UInt = #line
|
||||
) -> T? {
|
||||
switch result {
|
||||
case .success(let value):
|
||||
return value
|
||||
case .failure(let error):
|
||||
XCTFail("Expected success, but got failure: \(error)", file: file, line: line)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
111
camera_avfoundation/darwin/Tests/CameraZoomTests.swift
Normal file
111
camera_avfoundation/darwin/Tests/CameraZoomTests.swift
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class CameraZoomTests: XCTestCase {
|
||||
private func createCamera() -> (Camera, MockCaptureDevice) {
|
||||
let mockDevice = MockCaptureDevice()
|
||||
|
||||
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
||||
configuration.videoCaptureDeviceFactory = { _ in mockDevice }
|
||||
let camera = CameraTestUtils.createTestCamera(configuration)
|
||||
|
||||
return (camera, mockDevice)
|
||||
}
|
||||
|
||||
func testSetZoomLevel_setVideoZoomFactor() {
|
||||
let (camera, mockDevice) = createCamera()
|
||||
|
||||
mockDevice.maxAvailableVideoZoomFactor = 2.0
|
||||
mockDevice.minAvailableVideoZoomFactor = 0.0
|
||||
|
||||
let targetZoom = CGFloat(1.0)
|
||||
|
||||
var setVideoZoomFactorCalled = false
|
||||
mockDevice.setVideoZoomFactorStub = { zoom in
|
||||
XCTAssertEqual(zoom, targetZoom)
|
||||
setVideoZoomFactorCalled = true
|
||||
}
|
||||
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
camera.setZoomLevel(targetZoom) {
|
||||
result in
|
||||
let _ = self.assertSuccess(result)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30)
|
||||
|
||||
XCTAssertTrue(setVideoZoomFactorCalled)
|
||||
}
|
||||
|
||||
func testSetZoomLevel_returnsError_forZoomLevelBlowMinimum() {
|
||||
let (camera, mockDevice) = createCamera()
|
||||
|
||||
// Allowed zoom range between 2.0 and 3.0
|
||||
mockDevice.maxAvailableVideoZoomFactor = 2.0
|
||||
mockDevice.minAvailableVideoZoomFactor = 3.0
|
||||
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
camera.setZoomLevel(CGFloat(1.0)) { result in
|
||||
switch result {
|
||||
case .failure(let error as PigeonError):
|
||||
XCTAssertEqual(error.code, "ZOOM_ERROR")
|
||||
default:
|
||||
XCTFail("Expected failure")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30)
|
||||
}
|
||||
|
||||
func testSetZoomLevel_returnsError_forZoomLevelAboveMaximum() {
|
||||
let (camera, mockDevice) = createCamera()
|
||||
|
||||
// Allowed zoom range between 0.0 and 1.0
|
||||
mockDevice.maxAvailableVideoZoomFactor = 0.0
|
||||
mockDevice.minAvailableVideoZoomFactor = 1.0
|
||||
|
||||
let expectation = expectation(description: "Call completed")
|
||||
|
||||
camera.setZoomLevel(CGFloat(2.0)) { result in
|
||||
switch result {
|
||||
case .failure(let error as PigeonError):
|
||||
XCTAssertEqual(error.code, "ZOOM_ERROR")
|
||||
default:
|
||||
XCTFail("Expected failure")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30)
|
||||
}
|
||||
|
||||
func testMaximumAvailableZoomFactor_returnsDeviceMaxAvailableVideoZoomFactor() {
|
||||
let (camera, mockDevice) = createCamera()
|
||||
|
||||
let targetZoom = CGFloat(1.0)
|
||||
|
||||
mockDevice.maxAvailableVideoZoomFactor = CGFloat(targetZoom)
|
||||
|
||||
XCTAssertEqual(camera.maximumAvailableZoomFactor, targetZoom)
|
||||
}
|
||||
|
||||
func testMinimumAvailableZoomFactor_returnsDeviceMinAvailableVideoZoomFactor() {
|
||||
let (camera, mockDevice) = createCamera()
|
||||
|
||||
let targetZoom = CGFloat(1.0)
|
||||
|
||||
mockDevice.minAvailableVideoZoomFactor = CGFloat(targetZoom)
|
||||
|
||||
XCTAssertEqual(camera.minimumAvailableZoomFactor, targetZoom)
|
||||
}
|
||||
}
|
||||
33
camera_avfoundation/darwin/Tests/Mocks/MockAssetWriter.swift
Normal file
33
camera_avfoundation/darwin/Tests/Mocks/MockAssetWriter.swift
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// Mock implementation of `AssetWriter` protocol which allows injecting a custom
|
||||
/// implementation.
|
||||
final class MockAssetWriter: NSObject, AssetWriter {
|
||||
var statusStub: (() -> AVAssetWriter.Status)?
|
||||
var startWritingStub: (() -> Bool)?
|
||||
var finishWritingStub: ((() -> Void) -> Void)?
|
||||
|
||||
var status: AVAssetWriter.Status {
|
||||
return statusStub?() ?? .unknown
|
||||
}
|
||||
|
||||
var error: Error? = nil
|
||||
|
||||
func startWriting() -> Bool {
|
||||
return startWritingStub?() ?? true
|
||||
}
|
||||
|
||||
func finishWriting(completionHandler handler: @escaping () -> Void) {
|
||||
finishWritingStub?(handler)
|
||||
}
|
||||
|
||||
func startSession(atSourceTime startTime: CMTime) {}
|
||||
|
||||
func add(_ input: AVAssetWriterInput) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// Mock implementation of `AssetWriterInput` protocol which allows injecting a custom
|
||||
/// implementation.
|
||||
final class MockAssetWriterInput: NSObject, AssetWriterInput {
|
||||
var appendStub: ((CMSampleBuffer) -> Bool)?
|
||||
|
||||
var avInput: AVAssetWriterInput {
|
||||
preconditionFailure("Attempted to access unimplemented property: avInput")
|
||||
}
|
||||
|
||||
var expectsMediaDataInRealTime = false
|
||||
|
||||
var isReadyForMoreMediaData = false
|
||||
|
||||
func append(_ sampleBuffer: CMSampleBuffer) -> Bool {
|
||||
return appendStub?(sampleBuffer) ?? false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// Mock implementation of `AssetWriterInputPixelBufferAdaptor` protocol which allows injecting a custom
|
||||
/// implementation.
|
||||
final class MockAssetWriterInputPixelBufferAdaptor: NSObject, AssetWriterInputPixelBufferAdaptor {
|
||||
var appendStub: ((CVPixelBuffer, CMTime) -> Bool)?
|
||||
|
||||
func append(_ pixelBuffer: CVPixelBuffer, withPresentationTime presentationTime: CMTime) -> Bool {
|
||||
appendStub?(pixelBuffer, presentationTime) ?? true
|
||||
}
|
||||
}
|
||||
238
camera_avfoundation/darwin/Tests/Mocks/MockCamera.swift
Normal file
238
camera_avfoundation/darwin/Tests/Mocks/MockCamera.swift
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import Flutter
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class MockCamera: NSObject, Camera {
|
||||
var setDartApiStub: ((CameraEventApi?) -> Void)?
|
||||
var setOnFrameAvailableStub: (((() -> Void)?) -> Void)?
|
||||
var getMinimumExposureOffsetStub: (() -> CGFloat)?
|
||||
var getMaximumExposureOffsetStub: (() -> CGFloat)?
|
||||
var getMinimumAvailableZoomFactorStub: (() -> CGFloat)?
|
||||
var getMaximumAvailableZoomFactorStub: (() -> CGFloat)?
|
||||
var setUpCaptureSessionForAudioIfNeededStub: (() -> Void)?
|
||||
var receivedImageStreamDataStub: (() -> Void)?
|
||||
var startStub: (() -> Void)?
|
||||
var startVideoRecordingStub:
|
||||
((@escaping (Result<Void, any Error>) -> Void, FlutterBinaryMessenger?) -> Void)?
|
||||
var pauseVideoRecordingStub: (() -> Void)?
|
||||
var resumeVideoRecordingStub: (() -> Void)?
|
||||
var stopVideoRecordingStub: ((@escaping (Result<String, any Error>) -> Void) -> Void)?
|
||||
var captureToFileStub: ((@escaping (Result<String, any Error>) -> Void) -> Void)?
|
||||
var setDeviceOrientationStub: ((UIDeviceOrientation) -> Void)?
|
||||
var lockCaptureOrientationStub: ((PlatformDeviceOrientation) -> Void)?
|
||||
var unlockCaptureOrientationStub: (() -> Void)?
|
||||
var setImageFileFormatStub: ((PlatformImageFileFormat) -> Void)?
|
||||
var setJpegImageQualityStub: ((Int64) -> Void)?
|
||||
var setExposureModeStub: ((PlatformExposureMode) -> Void)?
|
||||
var setExposureOffsetStub: ((Double) -> Void)?
|
||||
var setExposurePointStub: ((PlatformPoint?, @escaping (Result<Void, any Error>) -> Void) -> Void)?
|
||||
var setFocusModeStub: ((PlatformFocusMode) -> Void)?
|
||||
var setFocusPointStub: ((PlatformPoint?, @escaping (Result<Void, any Error>) -> Void) -> Void)?
|
||||
var setZoomLevelStub: ((CGFloat, @escaping (Result<Void, any Error>) -> Void) -> Void)?
|
||||
var setFlashModeStub: ((PlatformFlashMode, @escaping (Result<Void, any Error>) -> Void) -> Void)?
|
||||
var pausePreviewStub: (() -> Void)?
|
||||
var resumePreviewStub: (() -> Void)?
|
||||
var setDescriptionWhileRecordingStub:
|
||||
((String, @escaping (Result<Void, any Error>) -> Void) -> Void)?
|
||||
var startImageStreamStub:
|
||||
((FlutterBinaryMessenger, @escaping (Result<Void, any Error>) -> Void) -> Void)?
|
||||
var stopImageStreamStub: (() -> Void)?
|
||||
var setVideoStabilizationModeStub:
|
||||
((PlatformVideoStabilizationMode, @escaping (Result<Void, any Error>) -> Void) -> Void)?
|
||||
var getIsVideoStabilizationModeSupportedStub: ((PlatformVideoStabilizationMode) -> Bool)?
|
||||
|
||||
var dartAPI: CameraEventApi? {
|
||||
get {
|
||||
preconditionFailure("Attempted to access unimplemented property: dartAPI")
|
||||
}
|
||||
set {
|
||||
setDartApiStub?(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
var onFrameAvailable: (() -> Void)? {
|
||||
get {
|
||||
preconditionFailure("Attempted to access unimplemented property: onFrameAvailable")
|
||||
}
|
||||
set {
|
||||
setOnFrameAvailableStub?(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
var videoFormat: FourCharCode = kCVPixelFormatType_32BGRA
|
||||
|
||||
var isPreviewPaused: Bool = false
|
||||
var isStreamingImages: Bool = false
|
||||
|
||||
var deviceOrientation: UIDeviceOrientation {
|
||||
get {
|
||||
preconditionFailure("Attempted to access unimplemented property: deviceOrientation")
|
||||
}
|
||||
set {
|
||||
setDeviceOrientationStub?(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
var minimumExposureOffset: CGFloat {
|
||||
return getMinimumExposureOffsetStub?() ?? 0
|
||||
}
|
||||
|
||||
var maximumExposureOffset: CGFloat {
|
||||
return getMaximumExposureOffsetStub?() ?? 0
|
||||
}
|
||||
|
||||
var minimumAvailableZoomFactor: CGFloat {
|
||||
return getMinimumAvailableZoomFactorStub?() ?? 0
|
||||
}
|
||||
|
||||
var maximumAvailableZoomFactor: CGFloat {
|
||||
return getMaximumAvailableZoomFactorStub?() ?? 0
|
||||
}
|
||||
|
||||
func setUpCaptureSessionForAudioIfNeeded() {
|
||||
setUpCaptureSessionForAudioIfNeededStub?()
|
||||
}
|
||||
|
||||
func reportInitializationState() {}
|
||||
|
||||
func receivedImageStreamData() {
|
||||
receivedImageStreamDataStub?()
|
||||
}
|
||||
|
||||
func start() {
|
||||
startStub?()
|
||||
}
|
||||
|
||||
func stop() {}
|
||||
|
||||
func startVideoRecording(
|
||||
completion: @escaping (Result<Void, any Error>) -> Void,
|
||||
messengerForStreaming messenger: FlutterBinaryMessenger?
|
||||
) {
|
||||
startVideoRecordingStub?(completion, messenger)
|
||||
}
|
||||
|
||||
func pauseVideoRecording() {
|
||||
pauseVideoRecordingStub?()
|
||||
}
|
||||
|
||||
func resumeVideoRecording() {
|
||||
resumeVideoRecordingStub?()
|
||||
}
|
||||
|
||||
func stopVideoRecording(completion: @escaping (Result<String, any Error>) -> Void) {
|
||||
stopVideoRecordingStub?(completion)
|
||||
}
|
||||
|
||||
func captureToFile(completion: @escaping (Result<String, any Error>) -> Void) {
|
||||
captureToFileStub?(completion)
|
||||
}
|
||||
|
||||
func lockCaptureOrientation(_ orientation: PlatformDeviceOrientation) {
|
||||
lockCaptureOrientationStub?(orientation)
|
||||
}
|
||||
|
||||
func unlockCaptureOrientation() {
|
||||
unlockCaptureOrientationStub?()
|
||||
}
|
||||
|
||||
func setImageFileFormat(_ fileFormat: PlatformImageFileFormat) {
|
||||
setImageFileFormatStub?(fileFormat)
|
||||
}
|
||||
|
||||
func setJpegImageQuality(_ quality: Int64) {
|
||||
setJpegImageQualityStub?(quality)
|
||||
}
|
||||
|
||||
func setExposureMode(_ mode: PlatformExposureMode) {
|
||||
setExposureModeStub?(mode)
|
||||
}
|
||||
|
||||
func setExposureOffset(_ offset: Double) {
|
||||
setExposureOffsetStub?(offset)
|
||||
}
|
||||
|
||||
func setExposurePoint(
|
||||
_ point: PlatformPoint?, withCompletion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
setExposurePointStub?(point, withCompletion)
|
||||
}
|
||||
|
||||
func setFocusMode(_ mode: PlatformFocusMode) {
|
||||
setFocusModeStub?(mode)
|
||||
}
|
||||
|
||||
func setFocusPoint(
|
||||
_ point: PlatformPoint?, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
setFocusPointStub?(point, completion)
|
||||
}
|
||||
|
||||
func setZoomLevel(
|
||||
_ zoom: CGFloat,
|
||||
withCompletion completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
setZoomLevelStub?(zoom, completion)
|
||||
}
|
||||
|
||||
func setFlashMode(
|
||||
_ mode: PlatformFlashMode,
|
||||
withCompletion completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
setFlashModeStub?(mode, completion)
|
||||
}
|
||||
|
||||
func pausePreview() {
|
||||
pausePreviewStub?()
|
||||
}
|
||||
|
||||
func resumePreview() {
|
||||
resumePreviewStub?()
|
||||
}
|
||||
|
||||
func setVideoStabilizationMode(
|
||||
_ mode: PlatformVideoStabilizationMode,
|
||||
withCompletion completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
setVideoStabilizationModeStub?(mode, completion)
|
||||
}
|
||||
|
||||
func isVideoStabilizationModeSupported(_ mode: PlatformVideoStabilizationMode) -> Bool {
|
||||
return getIsVideoStabilizationModeSupportedStub?(mode) ?? false
|
||||
}
|
||||
|
||||
func setDescriptionWhileRecording(
|
||||
_ cameraName: String,
|
||||
withCompletion completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
setDescriptionWhileRecordingStub?(cameraName, completion)
|
||||
}
|
||||
|
||||
func startImageStream(
|
||||
with messenger: FlutterBinaryMessenger,
|
||||
completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
startImageStreamStub?(messenger, completion)
|
||||
}
|
||||
|
||||
func stopImageStream() {
|
||||
stopImageStreamStub?()
|
||||
}
|
||||
|
||||
func captureOutput(
|
||||
_ output: AVCaptureOutput,
|
||||
didOutput sampleBuffer: CMSampleBuffer,
|
||||
from connection: AVCaptureConnection
|
||||
) {}
|
||||
|
||||
func close() {}
|
||||
|
||||
func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// Mock implementation of `CameraDeviceDiscoverer` protocol which allows injecting a custom
|
||||
/// implementation for session discovery.
|
||||
final class MockCameraDeviceDiscoverer: NSObject, CameraDeviceDiscoverer {
|
||||
var discoverySessionStub:
|
||||
(
|
||||
(
|
||||
_ deviceTypes: [AVCaptureDevice.DeviceType],
|
||||
_ mediaType: AVMediaType,
|
||||
_ position: AVCaptureDevice.Position
|
||||
) -> [NSObject & CaptureDevice]?
|
||||
)?
|
||||
|
||||
/// A stub that replaces the default implementation of
|
||||
/// `discoverySessionWithDeviceTypes:mediaType:position`.
|
||||
func discoverySession(
|
||||
withDeviceTypes deviceTypes: [AVCaptureDevice.DeviceType], mediaType: AVMediaType,
|
||||
position: AVCaptureDevice.Position
|
||||
) -> [CaptureDevice] {
|
||||
return discoverySessionStub?(deviceTypes, mediaType, position) ?? []
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// A mock implementation of `CaptureConnection` that allows injecting a custom implementation.
|
||||
final class MockCaptureConnection: NSObject, CaptureConnection {
|
||||
var setVideoOrientationStub: ((AVCaptureVideoOrientation) -> Void)?
|
||||
|
||||
var connection: AVCaptureConnection {
|
||||
preconditionFailure("Attempted to access unimplemented property: connection")
|
||||
}
|
||||
var isVideoMirrored = false
|
||||
var videoOrientation: AVCaptureVideoOrientation {
|
||||
get { AVCaptureVideoOrientation.portrait }
|
||||
set {
|
||||
setVideoOrientationStub?(newValue)
|
||||
}
|
||||
}
|
||||
var inputPorts: [AVCaptureInput.Port] = []
|
||||
var isVideoMirroringSupported = false
|
||||
var isVideoOrientationSupported = false
|
||||
var preferredVideoStabilizationMode = AVCaptureVideoStabilizationMode.off
|
||||
}
|
||||
127
camera_avfoundation/darwin/Tests/Mocks/MockCaptureDevice.swift
Normal file
127
camera_avfoundation/darwin/Tests/Mocks/MockCaptureDevice.swift
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// A mock implementation of `CaptureDevice` that allows mocking the class
|
||||
/// properties.
|
||||
class MockCaptureDevice: NSObject, CaptureDevice {
|
||||
var activeFormatStub: (() -> CaptureDeviceFormat)?
|
||||
var setActiveFormatStub: ((CaptureDeviceFormat) -> Void)?
|
||||
var getTorchModeStub: (() -> AVCaptureDevice.TorchMode)?
|
||||
var setTorchModeStub: ((AVCaptureDevice.TorchMode) -> Void)?
|
||||
var isFocusModeSupportedStub: ((AVCaptureDevice.FocusMode) -> Bool)?
|
||||
var setFocusModeStub: ((AVCaptureDevice.FocusMode) -> Void)?
|
||||
var setFocusPointOfInterestStub: ((CGPoint) -> Void)?
|
||||
var setExposureModeStub: ((AVCaptureDevice.ExposureMode) -> Void)?
|
||||
var setExposurePointOfInterestStub: ((CGPoint) -> Void)?
|
||||
var setExposureTargetBiasStub: ((Float, ((CMTime) -> Void)?) -> Void)?
|
||||
var isExposureModeSupportedStub: ((AVCaptureDevice.ExposureMode) -> Bool)?
|
||||
var setVideoZoomFactorStub: ((CGFloat) -> Void)?
|
||||
var lockForConfigurationStub: (() throws -> Void)?
|
||||
|
||||
var avDevice: AVCaptureDevice {
|
||||
preconditionFailure("Attempted to access unimplemented property: device")
|
||||
}
|
||||
|
||||
var uniqueID = ""
|
||||
var position = AVCaptureDevice.Position.unspecified
|
||||
var deviceType = AVCaptureDevice.DeviceType.builtInWideAngleCamera
|
||||
|
||||
var flutterActiveFormat: CaptureDeviceFormat {
|
||||
get {
|
||||
activeFormatStub?() ?? MockCaptureDeviceFormat()
|
||||
}
|
||||
set {
|
||||
setActiveFormatStub?(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
var flutterFormats: [CaptureDeviceFormat] = []
|
||||
var hasFlash = false
|
||||
var hasTorch = false
|
||||
var isTorchAvailable = false
|
||||
var torchMode: AVCaptureDevice.TorchMode {
|
||||
get {
|
||||
getTorchModeStub?() ?? .off
|
||||
}
|
||||
set {
|
||||
setTorchModeStub?(newValue)
|
||||
}
|
||||
}
|
||||
var isFocusPointOfInterestSupported = false
|
||||
var maxAvailableVideoZoomFactor = CGFloat(0)
|
||||
var minAvailableVideoZoomFactor = CGFloat(0)
|
||||
var videoZoomFactor: CGFloat {
|
||||
get { 0 }
|
||||
set {
|
||||
setVideoZoomFactorStub?(newValue)
|
||||
}
|
||||
}
|
||||
var isExposurePointOfInterestSupported = false
|
||||
var minExposureTargetBias = Float(0)
|
||||
var maxExposureTargetBias = Float(0)
|
||||
var activeVideoMinFrameDuration = CMTime(value: 1, timescale: 1)
|
||||
var activeVideoMaxFrameDuration = CMTime(value: 1, timescale: 1)
|
||||
|
||||
func isFlashModeSupported(_ mode: AVCaptureDevice.FlashMode) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func isFocusModeSupported(_ mode: AVCaptureDevice.FocusMode) -> Bool {
|
||||
return isFocusModeSupportedStub?(mode) ?? false
|
||||
}
|
||||
|
||||
var focusMode: AVCaptureDevice.FocusMode {
|
||||
get { .autoFocus }
|
||||
set { setFocusModeStub?(newValue) }
|
||||
}
|
||||
|
||||
func setFocusMode(_ focusMode: AVCaptureDevice.FocusMode) {
|
||||
setFocusModeStub?(focusMode)
|
||||
}
|
||||
|
||||
var focusPointOfInterest: CGPoint {
|
||||
get { CGPoint.zero }
|
||||
set { setFocusPointOfInterestStub?(newValue) }
|
||||
}
|
||||
|
||||
var exposureMode: AVCaptureDevice.ExposureMode {
|
||||
get { .autoExpose }
|
||||
set { setExposureModeStub?(newValue) }
|
||||
}
|
||||
|
||||
var exposurePointOfInterest: CGPoint {
|
||||
get { CGPoint.zero }
|
||||
set { setExposurePointOfInterestStub?(newValue) }
|
||||
}
|
||||
|
||||
func setExposureTargetBias(_ bias: Float, completionHandler handler: ((CMTime) -> Void)? = nil) {
|
||||
setExposureTargetBiasStub?(bias, handler)
|
||||
}
|
||||
|
||||
func isExposureModeSupported(_ mode: AVCaptureDevice.ExposureMode) -> Bool {
|
||||
return isExposureModeSupportedStub?(mode) ?? false
|
||||
}
|
||||
|
||||
var lensAperture: Float { 1.8 }
|
||||
|
||||
var exposureDuration: CMTime { CMTime(value: 1, timescale: 1) }
|
||||
|
||||
var iso: Float { 100 }
|
||||
|
||||
func isVideoStabilizationModeSupported(_ videoStabilizationMode: AVCaptureVideoStabilizationMode)
|
||||
-> Bool
|
||||
{
|
||||
return false
|
||||
}
|
||||
|
||||
func lockForConfiguration() throws {
|
||||
try lockForConfigurationStub?()
|
||||
}
|
||||
|
||||
func unlockForConfiguration() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// A mock implementation of `CaptureDeviceFormat` that allows mocking the class
|
||||
/// properties.
|
||||
final class MockCaptureDeviceFormat: NSObject, CaptureDeviceFormat {
|
||||
|
||||
/// The format associated with the capture device.
|
||||
var avFormat: AVCaptureDevice.Format {
|
||||
preconditionFailure("Attempted to access unimplemented property: avFormat")
|
||||
}
|
||||
|
||||
var _formatDescription: CMVideoFormatDescription?
|
||||
|
||||
/// The format description for the capture device.
|
||||
var formatDescription: CMFormatDescription {
|
||||
_formatDescription!
|
||||
}
|
||||
|
||||
/// The array of frame rate ranges supported by the video format.
|
||||
var flutterVideoSupportedFrameRateRanges: [FrameRateRange] = []
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
CMVideoFormatDescriptionCreate(
|
||||
allocator: kCFAllocatorDefault, codecType: kCVPixelFormatType_32BGRA, width: 1920,
|
||||
height: 1080, extensions: nil, formatDescriptionOut: &_formatDescription)
|
||||
}
|
||||
|
||||
init(codecType: OSType, width: Int32, height: Int32) {
|
||||
super.init()
|
||||
|
||||
CMVideoFormatDescriptionCreate(
|
||||
allocator: kCFAllocatorDefault, codecType: codecType, width: width,
|
||||
height: height, extensions: nil, formatDescriptionOut: &_formatDescription)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import Foundation
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
///// A mocked implementation of CaptureDeviceInputFactory which allows injecting a custom
|
||||
///// implementation.
|
||||
final class MockCaptureDeviceInputFactory: NSObject, CaptureDeviceInputFactory {
|
||||
func deviceInput(with device: CaptureDevice) throws -> CaptureInput {
|
||||
return MockCaptureInput()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// A mocked implementation of CaptureInput which allows injecting a custom
|
||||
/// implementation.
|
||||
final class MockCaptureInput: NSObject, CaptureInput {
|
||||
var avInput: AVCaptureInput {
|
||||
preconditionFailure("Attempted to access unimplemented property: input")
|
||||
}
|
||||
|
||||
var ports: [AVCaptureInput.Port] = []
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// Mock implementation of `CapturePhotoOutput` protocol which allows injecting a custom
|
||||
/// implementation.
|
||||
final class MockCapturePhotoOutput: NSObject, CapturePhotoOutput {
|
||||
var avOutput = AVCapturePhotoOutput()
|
||||
var availablePhotoCodecTypes: [AVVideoCodecType] = []
|
||||
var isHighResolutionCaptureEnabled = false
|
||||
var supportedFlashModes: [AVCaptureDevice.FlashMode] = []
|
||||
|
||||
// Stub that is called when the corresponding public method is called.
|
||||
var capturePhotoWithSettingsStub:
|
||||
((_ settings: AVCapturePhotoSettings, _ delegate: AVCapturePhotoCaptureDelegate) -> Void)?
|
||||
|
||||
// Stub that is called when the corresponding public method is called.
|
||||
var connectionWithMediaTypeStub: ((_ mediaType: AVMediaType) -> CaptureConnection?)?
|
||||
|
||||
func capturePhoto(with settings: AVCapturePhotoSettings, delegate: AVCapturePhotoCaptureDelegate)
|
||||
{
|
||||
capturePhotoWithSettingsStub?(settings, delegate)
|
||||
}
|
||||
|
||||
func connection(with mediaType: AVMediaType) -> CaptureConnection? {
|
||||
return connectionWithMediaTypeStub?(mediaType)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// Mock implementation of `CaptureSession` protocol which allows injecting a custom
|
||||
/// implementation.
|
||||
final class MockCaptureSession: NSObject, CaptureSession {
|
||||
var setSessionPresetStub: ((AVCaptureSession.Preset) -> Void)?
|
||||
var beginConfigurationStub: (() -> Void)?
|
||||
var commitConfigurationStub: (() -> Void)?
|
||||
var startRunningStub: (() -> Void)?
|
||||
var stopRunningStub: (() -> Void)?
|
||||
var canSetSessionPresetStub: ((AVCaptureSession.Preset) -> Bool)?
|
||||
|
||||
var _sessionPreset = AVCaptureSession.Preset.high
|
||||
var inputs = [AVCaptureInput]()
|
||||
var outputs = [AVCaptureOutput]()
|
||||
|
||||
private(set) var addedAudioOutputCount: Int = 0
|
||||
|
||||
var automaticallyConfiguresApplicationAudioSession = false
|
||||
var isRunning = true
|
||||
|
||||
var sessionPreset: AVCaptureSession.Preset {
|
||||
get {
|
||||
return _sessionPreset
|
||||
}
|
||||
set {
|
||||
setSessionPresetStub?(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
func beginConfiguration() {
|
||||
beginConfigurationStub?()
|
||||
}
|
||||
|
||||
func commitConfiguration() {
|
||||
commitConfigurationStub?()
|
||||
}
|
||||
|
||||
func startRunning() {
|
||||
startRunningStub?()
|
||||
}
|
||||
|
||||
func stopRunning() {
|
||||
stopRunningStub?()
|
||||
}
|
||||
|
||||
func canSetSessionPreset(_ preset: AVCaptureSession.Preset) -> Bool {
|
||||
return canSetSessionPresetStub?(preset) ?? true
|
||||
}
|
||||
|
||||
func addInputWithNoConnections(_ input: CaptureInput) {}
|
||||
|
||||
func addOutputWithNoConnections(_ output: AVCaptureOutput) {}
|
||||
|
||||
func addConnection(_: AVCaptureConnection) {}
|
||||
|
||||
func addInput(_: CaptureInput) {}
|
||||
|
||||
func addOutput(_ output: AVCaptureOutput) {
|
||||
|
||||
if output is AVCaptureAudioDataOutput {
|
||||
addedAudioOutputCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
func removeInput(_: CaptureInput) {}
|
||||
|
||||
func removeOutput(_: AVCaptureOutput) {}
|
||||
|
||||
func canAddInput(_: CaptureInput) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func canAddOutput(_: AVCaptureOutput) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func canAddConnection(_: AVCaptureConnection) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// Mock implementation of `CaptureVideoDataOutput` protocol which allows injecting a custom
|
||||
/// implementation.
|
||||
class MockCaptureVideoDataOutput: NSObject, CaptureVideoDataOutput {
|
||||
var avOutput = AVCaptureVideoDataOutput()
|
||||
var alwaysDiscardsLateVideoFrames = false
|
||||
var videoSettings: [String: Any]! = [:]
|
||||
var availableVideoPixelFormatTypes: [FourCharCode] = []
|
||||
|
||||
var connectionWithMediaTypeStub: ((AVMediaType) -> CaptureConnection?)?
|
||||
|
||||
func connection(with mediaType: AVMediaType) -> CaptureConnection? {
|
||||
return connectionWithMediaTypeStub?(mediaType)
|
||||
}
|
||||
|
||||
func setSampleBufferDelegate(
|
||||
_ sampleBufferDelegate: AVCaptureVideoDataOutputSampleBufferDelegate?,
|
||||
queue sampleBufferCallbackQueue: DispatchQueue?
|
||||
) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import UIKit
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class MockDeviceOrientationProvider: NSObject, DeviceOrientationProvider {
|
||||
var orientationStub: (() -> UIDeviceOrientation)?
|
||||
|
||||
var orientation: UIDeviceOrientation {
|
||||
return orientationStub?() ?? .unknown
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import Flutter
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class MockCameraPermissionManager: CameraPermissionManager {
|
||||
var requestCameraPermissionStub: ((@escaping CameraPermissionRequestCompletionHandler) -> Void)?
|
||||
var requestAudioPermissionStub: ((@escaping CameraPermissionRequestCompletionHandler) -> Void)?
|
||||
|
||||
init() {
|
||||
super.init(permissionService: DefaultPermissionService())
|
||||
}
|
||||
|
||||
override func requestCameraPermission(completionHandler: @escaping (PigeonError?) -> Void) {
|
||||
if let stub = requestCameraPermissionStub {
|
||||
stub(completionHandler)
|
||||
} else {
|
||||
super.requestCameraPermission(completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
override func requestAudioPermission(completionHandler: @escaping (PigeonError?) -> Void) {
|
||||
if let stub = requestAudioPermissionStub {
|
||||
stub(completionHandler)
|
||||
} else {
|
||||
super.requestAudioPermission(completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import Flutter
|
||||
|
||||
/// Mocked implementation of `FlutterBinaryMessenger` protocol that exists to allow constructing
|
||||
/// a `CameraPlugin` instance for testing. It contains an empty implementation for all protocol
|
||||
/// methods.
|
||||
final class MockFlutterBinaryMessenger: NSObject, FlutterBinaryMessenger {
|
||||
func send(onChannel channel: String, message: Data?) {}
|
||||
|
||||
func send(
|
||||
onChannel channel:
|
||||
String,
|
||||
message:
|
||||
Data?, binaryReply callback: FlutterBinaryReply? = nil
|
||||
) {}
|
||||
|
||||
func setMessageHandlerOnChannel(
|
||||
_ channel: String,
|
||||
binaryMessageHandler handler: FlutterBinaryMessageHandler? = nil
|
||||
) -> FlutterBinaryMessengerConnection { 0 }
|
||||
|
||||
func cleanUpConnection(_ connection: FlutterBinaryMessengerConnection) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import Flutter
|
||||
|
||||
/// Mocked implementation of `FlutterTextureRegistry` protocol that exists to allow constructing
|
||||
/// a `CameraPlugin` instance for testing. It contains an empty implementation for all protocol
|
||||
/// methods.
|
||||
final class MockFlutterTextureRegistry: NSObject, FlutterTextureRegistry {
|
||||
func register(_ texture: FlutterTexture) -> Int64 { 0 }
|
||||
|
||||
func textureFrameAvailable(_ textureId: Int64) {}
|
||||
|
||||
func unregisterTexture(_ textureId: Int64) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import Foundation
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// A mock implementation of `FrameRateRange` that allows mocking the class properties.
|
||||
final class MockFrameRateRange: NSObject, FrameRateRange {
|
||||
var minFrameRate: Float64
|
||||
var maxFrameRate: Float64
|
||||
|
||||
/// Initializes a `MockFrameRateRange` with the given frame rate range.
|
||||
init(minFrameRate: Float64, maxFrameRate: Float64) {
|
||||
self.minFrameRate = minFrameRate
|
||||
self.maxFrameRate = maxFrameRate
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import Flutter
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// A mock implementation of `CameraGlobalEventApi` that captures received
|
||||
/// `deviceOrientationChanged` events and exposes whether they were received to the testing code.
|
||||
final class MockGlobalEventApi: CameraGlobalEventApiProtocol {
|
||||
/// Whether the `deviceOrientationChanged` callback was called.
|
||||
var deviceOrientationChangedCalled = false
|
||||
|
||||
/// The last orientation received by the `deviceOrientationChanged` callback.
|
||||
var lastOrientation = PlatformDeviceOrientation.portraitUp
|
||||
|
||||
func deviceOrientationChanged(
|
||||
orientation orientationArg: PlatformDeviceOrientation,
|
||||
completion: @escaping (Result<Void, PigeonError>) -> Void
|
||||
) {
|
||||
deviceOrientationChangedCalled = true
|
||||
lastOrientation = orientationArg
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import Foundation
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// A mock implementation of `WritableData` that allows injecting a custom implementation
|
||||
/// for writing to a file.
|
||||
final class MockWritableData: WritableData {
|
||||
var writeToFileStub: ((String, Data.WritingOptions) throws -> Void)?
|
||||
|
||||
func writeToPath(_ path: String, options: Data.WritingOptions) throws {
|
||||
if let stub = self.writeToFileStub {
|
||||
try stub(path, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
248
camera_avfoundation/darwin/Tests/PhotoCaptureTests.swift
Normal file
248
camera_avfoundation/darwin/Tests/PhotoCaptureTests.swift
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
/// Includes test cases related to photo capture operations for Camera class.
|
||||
final class PhotoCaptureTests: XCTestCase {
|
||||
private func createCam(with captureSessionQueue: DispatchQueue) -> DefaultCamera {
|
||||
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
||||
configuration.captureSessionQueue = captureSessionQueue
|
||||
return CameraTestUtils.createTestCamera(configuration)
|
||||
}
|
||||
|
||||
func testCaptureToFile_mustReportErrorToResultIfSavePhotoDelegateCompletionsWithError() {
|
||||
let errorExpectation = expectation(
|
||||
description: "Must send error to result if save photo delegate completes with error.")
|
||||
let captureSessionQueue = DispatchQueue(label: "capture_session_queue")
|
||||
captureSessionQueue.setSpecific(
|
||||
key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue)
|
||||
let cam = createCam(with: captureSessionQueue)
|
||||
let error = NSError(domain: "test", code: 0, userInfo: nil)
|
||||
|
||||
let mockOutput = MockCapturePhotoOutput()
|
||||
mockOutput.capturePhotoWithSettingsStub = { settings, photoDelegate in
|
||||
let delegate =
|
||||
cam.inProgressSavePhotoDelegates[settings.uniqueID]
|
||||
// Completion runs on IO queue.
|
||||
let ioQueue = DispatchQueue(label: "io_queue")
|
||||
ioQueue.async {
|
||||
delegate?.completionHandler(nil, error)
|
||||
}
|
||||
}
|
||||
cam.capturePhotoOutput = mockOutput
|
||||
|
||||
// `Camera.captureToFile` runs on capture session queue.
|
||||
captureSessionQueue.async {
|
||||
cam.captureToFile { result in
|
||||
switch result {
|
||||
case .success(_):
|
||||
XCTFail("Expected failure")
|
||||
case .failure(_):
|
||||
break
|
||||
}
|
||||
errorExpectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWithPath() {
|
||||
let pathExpectation = expectation(
|
||||
description: "Must send file path to result if save photo delegate completes with file path.")
|
||||
let captureSessionQueue = DispatchQueue(label: "capture_session_queue")
|
||||
captureSessionQueue.setSpecific(
|
||||
key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue)
|
||||
let cam = createCam(with: captureSessionQueue)
|
||||
let filePath = "test"
|
||||
|
||||
let mockOutput = MockCapturePhotoOutput()
|
||||
mockOutput.capturePhotoWithSettingsStub = { settings, photoDelegate in
|
||||
let delegate =
|
||||
cam.inProgressSavePhotoDelegates[settings.uniqueID]
|
||||
// Completion runs on IO queue.
|
||||
let ioQueue = DispatchQueue(label: "io_queue")
|
||||
ioQueue.async {
|
||||
delegate?.completionHandler(filePath, nil)
|
||||
}
|
||||
}
|
||||
cam.capturePhotoOutput = mockOutput
|
||||
|
||||
// `Camera.captureToFile` runs on capture session queue.
|
||||
captureSessionQueue.async {
|
||||
cam.captureToFile { result in
|
||||
switch result {
|
||||
case .success(let result):
|
||||
XCTAssertEqual(result, filePath)
|
||||
case .failure(_):
|
||||
XCTFail("Unexpected failure")
|
||||
}
|
||||
pathExpectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testCaptureToFile_mustReportFileExtensionWithHeifWhenHEVCIsAvailableAndFileFormatIsHEIF() {
|
||||
let expectation = self.expectation(
|
||||
description: "Test must set extension to heif if availablePhotoCodecTypes contains HEVC.")
|
||||
|
||||
let captureSessionQueue = DispatchQueue(label: "capture_session_queue")
|
||||
captureSessionQueue.setSpecific(
|
||||
key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue)
|
||||
let cam = createCam(with: captureSessionQueue)
|
||||
cam.setImageFileFormat(PlatformImageFileFormat.heif)
|
||||
|
||||
let mockOutput = MockCapturePhotoOutput()
|
||||
mockOutput.availablePhotoCodecTypes = [AVVideoCodecType.hevc]
|
||||
mockOutput.capturePhotoWithSettingsStub = { settings, photoDelegate in
|
||||
let delegate =
|
||||
cam.inProgressSavePhotoDelegates[settings.uniqueID]
|
||||
// Completion runs on IO queue.
|
||||
let ioQueue = DispatchQueue(label: "io_queue")
|
||||
ioQueue.async {
|
||||
delegate?.completionHandler(delegate?.filePath, nil)
|
||||
}
|
||||
}
|
||||
cam.capturePhotoOutput = mockOutput
|
||||
|
||||
// `Camera.captureToFile` runs on capture session queue.
|
||||
captureSessionQueue.async {
|
||||
cam.captureToFile { result in
|
||||
if let filePath = self.assertSuccess(result) {
|
||||
XCTAssertEqual((filePath as NSString).pathExtension, "heif")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testCaptureToFile_mustReportFileExtensionWithJpgWhenHEVCNotAvailableAndFileFormatIsHEIF() {
|
||||
let expectation = self.expectation(
|
||||
description:
|
||||
"Test must set extension to jpg if availablePhotoCodecTypes does not contain HEVC.")
|
||||
|
||||
let captureSessionQueue = DispatchQueue(label: "capture_session_queue")
|
||||
captureSessionQueue.setSpecific(
|
||||
key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue)
|
||||
let cam = createCam(with: captureSessionQueue)
|
||||
cam.setImageFileFormat(PlatformImageFileFormat.heif)
|
||||
|
||||
let mockOutput = MockCapturePhotoOutput()
|
||||
mockOutput.capturePhotoWithSettingsStub = { settings, photoDelegate in
|
||||
let delegate =
|
||||
cam.inProgressSavePhotoDelegates[settings.uniqueID]
|
||||
// Completion runs on IO queue.
|
||||
let ioQueue = DispatchQueue(label: "io_queue")
|
||||
ioQueue.async {
|
||||
delegate?.completionHandler(delegate?.filePath, nil)
|
||||
}
|
||||
}
|
||||
cam.capturePhotoOutput = mockOutput
|
||||
|
||||
// `Camera.captureToFile` runs on capture session queue.
|
||||
captureSessionQueue.async {
|
||||
cam.captureToFile { result in
|
||||
if let filePath = self.assertSuccess(result) {
|
||||
XCTAssertEqual((filePath as NSString).pathExtension, "jpg")
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testCaptureToFile_handlesTorchMode() {
|
||||
let pathExpectation = expectation(
|
||||
description: "Must send file path to result if save photo delegate completes with file path.")
|
||||
let setTorchExpectation = expectation(
|
||||
description: "Should set torch mode to AVCaptureTorchModeOn.")
|
||||
|
||||
let captureDeviceMock = MockCaptureDevice()
|
||||
captureDeviceMock.hasTorch = true
|
||||
captureDeviceMock.isTorchAvailable = true
|
||||
captureDeviceMock.getTorchModeStub = { .auto }
|
||||
captureDeviceMock.setTorchModeStub = { mode in
|
||||
if mode == .on {
|
||||
setTorchExpectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
let captureSessionQueue = DispatchQueue(label: "capture_session_queue")
|
||||
captureSessionQueue.setSpecific(
|
||||
key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue)
|
||||
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
||||
configuration.captureSessionQueue = captureSessionQueue
|
||||
configuration.videoCaptureDeviceFactory = { _ in captureDeviceMock }
|
||||
let cam = CameraTestUtils.createTestCamera(configuration)
|
||||
|
||||
let filePath = "test"
|
||||
let mockOutput = MockCapturePhotoOutput()
|
||||
mockOutput.capturePhotoWithSettingsStub = { settings, photoDelegate in
|
||||
let delegate =
|
||||
cam.inProgressSavePhotoDelegates[settings.uniqueID]
|
||||
// Completion runs on IO queue.
|
||||
let ioQueue = DispatchQueue(label: "io_queue")
|
||||
ioQueue.async {
|
||||
delegate?.completionHandler(filePath, nil)
|
||||
}
|
||||
}
|
||||
cam.capturePhotoOutput = mockOutput
|
||||
|
||||
// `Camera.captureToFile` runs on capture session queue.
|
||||
captureSessionQueue.async {
|
||||
cam.setFlashMode(.torch) { _ in }
|
||||
cam.captureToFile { result in
|
||||
if let result = self.assertSuccess(result) {
|
||||
XCTAssertEqual(result, filePath)
|
||||
}
|
||||
pathExpectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testCaptureToFile_mustSavePhotoToCameraPicturesDirectory() {
|
||||
let expectedPath = "/tmp/camera/pictures/"
|
||||
let expectation = self.expectation(
|
||||
description: "Photo must be saved to \(expectedPath) directory.")
|
||||
|
||||
let captureSessionQueue = DispatchQueue(label: "capture_session_queue")
|
||||
captureSessionQueue.setSpecific(
|
||||
key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue)
|
||||
let cam = createCam(with: captureSessionQueue)
|
||||
|
||||
let mockOutput = MockCapturePhotoOutput()
|
||||
mockOutput.capturePhotoWithSettingsStub = { settings, photoDelegate in
|
||||
let delegate = cam.inProgressSavePhotoDelegates[settings.uniqueID]
|
||||
let ioQueue = DispatchQueue(label: "io_queue")
|
||||
ioQueue.async {
|
||||
delegate?.completionHandler(delegate?.filePath, nil)
|
||||
}
|
||||
}
|
||||
cam.capturePhotoOutput = mockOutput
|
||||
|
||||
captureSessionQueue.async {
|
||||
cam.captureToFile { result in
|
||||
if let filePath = self.assertSuccess(result) {
|
||||
XCTAssertTrue(
|
||||
filePath.contains(expectedPath)
|
||||
)
|
||||
}
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
}
|
||||
35
camera_avfoundation/darwin/Tests/QueueUtilsTests.swift
Normal file
35
camera_avfoundation/darwin/Tests/QueueUtilsTests.swift
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class QueueUtilsTests: XCTestCase {
|
||||
func testShouldStayOnMainQueueIfCalledFromMainQueue() {
|
||||
let expectation = expectation(description: "Block must be run on the main queue")
|
||||
|
||||
ensureToRunOnMainQueue {
|
||||
if Thread.isMainThread {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30)
|
||||
}
|
||||
|
||||
func testShouldDispatchToMainQueueIfCalledFromBackgroundQueue() {
|
||||
let expectation = expectation(description: "Block must be run on the main queue")
|
||||
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
ensureToRunOnMainQueue {
|
||||
if Thread.isMainThread {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30)
|
||||
}
|
||||
}
|
||||
542
camera_avfoundation/darwin/Tests/SampleBufferTests.swift
Normal file
542
camera_avfoundation/darwin/Tests/SampleBufferTests.swift
Normal file
|
|
@ -0,0 +1,542 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
private class FakeMediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper {
|
||||
let inputMock: MockAssetWriterInput
|
||||
|
||||
init(inputMock: MockAssetWriterInput) {
|
||||
self.inputMock = inputMock
|
||||
}
|
||||
|
||||
override func lockDevice(_ captureDevice: CaptureDevice) throws {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
override func unlockDevice(_ captureDevice: CaptureDevice) {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
override func beginConfiguration(for videoCaptureSession: CaptureSession) {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
override func commitConfiguration(for videoCaptureSession: CaptureSession) {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
override func setMinFrameDuration(_ duration: CMTime, on captureDevice: CaptureDevice) {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
override func setMaxFrameDuration(_ duration: CMTime, on captureDevice: CaptureDevice) {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
override func assetWriterAudioInput(withOutputSettings outputSettings: [String: Any]?)
|
||||
-> AssetWriterInput
|
||||
{
|
||||
return inputMock
|
||||
}
|
||||
|
||||
override func assetWriterVideoInput(withOutputSettings outputSettings: [String: Any]?)
|
||||
-> AssetWriterInput
|
||||
{
|
||||
return inputMock
|
||||
}
|
||||
|
||||
override func addInput(_ writerInput: AssetWriterInput, to writer: AssetWriter) {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
override func recommendedVideoSettingsForAssetWriter(
|
||||
withFileType fileType: AVFileType, for output: CaptureVideoDataOutput
|
||||
) -> [String: Any]? {
|
||||
return [:]
|
||||
}
|
||||
}
|
||||
|
||||
/// Includes test cases related to sample buffer handling for Camera class.
|
||||
final class CameraSampleBufferTests: XCTestCase {
|
||||
private func createCamera() -> (
|
||||
DefaultCamera,
|
||||
MockAssetWriter,
|
||||
MockAssetWriterInputPixelBufferAdaptor,
|
||||
MockAssetWriterInput
|
||||
) {
|
||||
let assetWriter = MockAssetWriter()
|
||||
let adaptor = MockAssetWriterInputPixelBufferAdaptor()
|
||||
let input = MockAssetWriterInput()
|
||||
|
||||
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
||||
configuration.mediaSettings = PlatformMediaSettings(
|
||||
resolutionPreset: .medium,
|
||||
framesPerSecond: nil,
|
||||
videoBitrate: nil,
|
||||
audioBitrate: nil,
|
||||
enableAudio: true)
|
||||
configuration.mediaSettingsWrapper = FakeMediaSettingsAVWrapper(inputMock: input)
|
||||
|
||||
configuration.assetWriterFactory = { url, fileType in
|
||||
return assetWriter
|
||||
}
|
||||
configuration.inputPixelBufferAdaptorFactory = { input, settings in
|
||||
return adaptor
|
||||
}
|
||||
|
||||
return (
|
||||
CameraTestUtils.createTestCamera(configuration),
|
||||
assetWriter,
|
||||
adaptor,
|
||||
input
|
||||
)
|
||||
}
|
||||
|
||||
func testSampleBufferCallbackQueueMustBeCaptureSessionQueue() {
|
||||
let captureSessionQueue = DispatchQueue(label: "testing")
|
||||
let camera = CameraTestUtils.createCameraWithCaptureSessionQueue(captureSessionQueue)
|
||||
XCTAssertEqual(
|
||||
captureSessionQueue, camera.captureVideoOutput.avOutput.sampleBufferCallbackQueue,
|
||||
"Sample buffer callback queue must be the capture session queue.")
|
||||
}
|
||||
|
||||
func testCopyPixelBuffer() {
|
||||
let (camera, _, _, _) = createCamera()
|
||||
let capturedSampleBuffer = CameraTestUtils.createTestSampleBuffer()
|
||||
let capturedPixelBuffer = CMSampleBufferGetImageBuffer(capturedSampleBuffer)!
|
||||
let testConnection = CameraTestUtils.createTestConnection(camera.captureVideoOutput.avOutput)
|
||||
|
||||
// Mimic sample buffer callback when captured a new video sample.
|
||||
camera.captureOutput(
|
||||
camera.captureVideoOutput.avOutput,
|
||||
didOutput: capturedSampleBuffer,
|
||||
from: testConnection)
|
||||
let deliveredPixelBuffer = camera.copyPixelBuffer()?.takeRetainedValue()
|
||||
XCTAssertEqual(
|
||||
deliveredPixelBuffer, capturedPixelBuffer,
|
||||
"Camera must deliver the latest captured pixel buffer to copyPixelBuffer API.")
|
||||
}
|
||||
|
||||
func testDidOutputSampleBuffer_mustNotChangeSampleBufferRetainCountAfterPauseResumeRecording() {
|
||||
let (camera, _, _, _) = createCamera()
|
||||
let sampleBuffer = CameraTestUtils.createTestSampleBuffer()
|
||||
let testConnection = CameraTestUtils.createTestConnection(camera.captureVideoOutput.avOutput)
|
||||
|
||||
let initialRetainCount = CFGetRetainCount(sampleBuffer)
|
||||
|
||||
// Pause then resume the recording.
|
||||
camera.startVideoRecording(completion: { error in }, messengerForStreaming: nil)
|
||||
camera.pauseVideoRecording()
|
||||
camera.resumeVideoRecording()
|
||||
|
||||
camera.captureOutput(
|
||||
camera.captureVideoOutput.avOutput,
|
||||
didOutput: sampleBuffer,
|
||||
from: testConnection)
|
||||
|
||||
let finalRetainCount = CFGetRetainCount(sampleBuffer)
|
||||
XCTAssertEqual(
|
||||
finalRetainCount, initialRetainCount,
|
||||
"didOutputSampleBuffer must not change the sample buffer retain count after pause resume recording."
|
||||
)
|
||||
}
|
||||
|
||||
func testDidOutputSampleBufferIgnoreAudioSamplesBeforeVideoSamples() {
|
||||
let (camera, writerMock, adaptorMock, inputMock) = createCamera()
|
||||
var status = AVAssetWriter.Status.unknown
|
||||
writerMock.startWritingStub = {
|
||||
status = .writing
|
||||
return true
|
||||
}
|
||||
writerMock.statusStub = {
|
||||
return status
|
||||
}
|
||||
|
||||
let videoSample = CameraTestUtils.createTestSampleBuffer()
|
||||
let testVideoConnection = CameraTestUtils.createTestConnection(
|
||||
camera.captureVideoOutput.avOutput)
|
||||
|
||||
let audioSample = CameraTestUtils.createTestAudioSampleBuffer()
|
||||
let testAudioOutput = CameraTestUtils.createTestAudioOutput()
|
||||
let testAudioConnection = CameraTestUtils.createTestConnection(testAudioOutput)
|
||||
|
||||
var writtenSamples: [String] = []
|
||||
adaptorMock.appendStub = { buffer, time in
|
||||
writtenSamples.append("video")
|
||||
return true
|
||||
}
|
||||
inputMock.isReadyForMoreMediaData = true
|
||||
inputMock.appendStub = { buffer in
|
||||
writtenSamples.append("audio")
|
||||
return true
|
||||
}
|
||||
|
||||
camera.startVideoRecording(completion: { error in }, messengerForStreaming: nil)
|
||||
camera.captureOutput(testAudioOutput, didOutput: audioSample, from: testAudioConnection)
|
||||
camera.captureOutput(testAudioOutput, didOutput: audioSample, from: testAudioConnection)
|
||||
camera.captureOutput(
|
||||
camera.captureVideoOutput.avOutput,
|
||||
didOutput: videoSample,
|
||||
from: testVideoConnection)
|
||||
camera.captureOutput(testAudioOutput, didOutput: audioSample, from: testAudioConnection)
|
||||
|
||||
let expectedSamples = ["video", "audio"]
|
||||
XCTAssertEqual(writtenSamples, expectedSamples, "First appended sample must be video.")
|
||||
}
|
||||
|
||||
func testDidOutputSampleBufferSampleTimesMustBeNumericAfterPauseResume() {
|
||||
let (camera, writerMock, adaptorMock, inputMock) = createCamera()
|
||||
|
||||
let videoSample = CameraTestUtils.createTestSampleBuffer()
|
||||
let testVideoConnection = CameraTestUtils.createTestConnection(
|
||||
camera.captureVideoOutput.avOutput)
|
||||
|
||||
let audioSample = CameraTestUtils.createTestAudioSampleBuffer()
|
||||
let testAudioOutput = CameraTestUtils.createTestAudioOutput()
|
||||
let testAudioConnection = CameraTestUtils.createTestConnection(testAudioOutput)
|
||||
|
||||
var status = AVAssetWriter.Status.unknown
|
||||
writerMock.startWritingStub = {
|
||||
status = .writing
|
||||
return true
|
||||
}
|
||||
writerMock.statusStub = {
|
||||
return status
|
||||
}
|
||||
|
||||
var videoAppended = false
|
||||
adaptorMock.appendStub = { buffer, time in
|
||||
XCTAssert(CMTIME_IS_NUMERIC(time))
|
||||
videoAppended = true
|
||||
return true
|
||||
}
|
||||
|
||||
var audioAppended = false
|
||||
inputMock.isReadyForMoreMediaData = true
|
||||
inputMock.appendStub = { buffer in
|
||||
let sampleTime = CMSampleBufferGetPresentationTimeStamp(buffer)
|
||||
XCTAssert(CMTIME_IS_NUMERIC(sampleTime))
|
||||
audioAppended = true
|
||||
return true
|
||||
}
|
||||
|
||||
camera.startVideoRecording(completion: { error in }, messengerForStreaming: nil)
|
||||
camera.pauseVideoRecording()
|
||||
camera.resumeVideoRecording()
|
||||
camera.captureOutput(
|
||||
camera.captureVideoOutput.avOutput,
|
||||
didOutput: videoSample,
|
||||
from: testVideoConnection)
|
||||
camera.captureOutput(testAudioOutput, didOutput: audioSample, from: testAudioConnection)
|
||||
camera.captureOutput(
|
||||
camera.captureVideoOutput.avOutput,
|
||||
didOutput: videoSample,
|
||||
from: testVideoConnection)
|
||||
camera.captureOutput(testAudioOutput, didOutput: audioSample, from: testAudioConnection)
|
||||
|
||||
XCTAssert(videoAppended && audioAppended, "Video or audio was not appended.")
|
||||
}
|
||||
|
||||
func testDidOutputSampleBufferMustNotAppendSampleWhenReadyForMoreMediaDataIsFalse() {
|
||||
let (camera, _, adaptorMock, inputMock) = createCamera()
|
||||
|
||||
let videoSample = CameraTestUtils.createTestSampleBuffer()
|
||||
let testVideoConnection = CameraTestUtils.createTestConnection(
|
||||
camera.captureVideoOutput.avOutput)
|
||||
|
||||
var sampleAppended = false
|
||||
adaptorMock.appendStub = { buffer, time in
|
||||
sampleAppended = true
|
||||
return true
|
||||
}
|
||||
|
||||
camera.startVideoRecording(completion: { error in }, messengerForStreaming: nil)
|
||||
|
||||
inputMock.isReadyForMoreMediaData = true
|
||||
sampleAppended = false
|
||||
camera.captureOutput(
|
||||
camera.captureVideoOutput.avOutput,
|
||||
didOutput: videoSample,
|
||||
from: testVideoConnection)
|
||||
XCTAssertTrue(sampleAppended, "Sample was not appended.")
|
||||
|
||||
inputMock.isReadyForMoreMediaData = false
|
||||
sampleAppended = false
|
||||
camera.captureOutput(
|
||||
camera.captureVideoOutput.avOutput,
|
||||
didOutput: videoSample,
|
||||
from: testVideoConnection)
|
||||
XCTAssertFalse(sampleAppended, "Sample cannot be appended when readyForMoreMediaData is NO.")
|
||||
}
|
||||
|
||||
func testStopVideoRecordingWithCompletionMustCallCompletion() {
|
||||
let (camera, writerMock, _, _) = createCamera()
|
||||
|
||||
var status = AVAssetWriter.Status.unknown
|
||||
writerMock.startWritingStub = {
|
||||
status = .writing
|
||||
return true
|
||||
}
|
||||
writerMock.statusStub = {
|
||||
return status
|
||||
}
|
||||
writerMock.finishWritingStub = { handler in
|
||||
XCTAssert(
|
||||
writerMock.status == .writing,
|
||||
"Cannot call finishWritingWithCompletionHandler when status is not AVAssetWriter.Status.writing."
|
||||
)
|
||||
handler()
|
||||
}
|
||||
|
||||
camera.startVideoRecording(completion: { error in }, messengerForStreaming: nil)
|
||||
var completionCalled = false
|
||||
camera.stopVideoRecording(completion: { result in
|
||||
completionCalled = true
|
||||
})
|
||||
|
||||
XCTAssert(completionCalled, "Completion was not called.")
|
||||
}
|
||||
|
||||
func testStartWritingShouldNotBeCalledBetweenSampleCreationAndAppending() {
|
||||
let (camera, writerMock, adaptorMock, inputMock) = createCamera()
|
||||
|
||||
let videoSample = CameraTestUtils.createTestSampleBuffer()
|
||||
let testVideoConnection = CameraTestUtils.createTestConnection(
|
||||
camera.captureVideoOutput.avOutput)
|
||||
|
||||
var startWritingCalled = false
|
||||
writerMock.startWritingStub = {
|
||||
startWritingCalled = true
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
var videoAppended = false
|
||||
adaptorMock.appendStub = { buffer, time in
|
||||
videoAppended = true
|
||||
return true
|
||||
}
|
||||
|
||||
inputMock.isReadyForMoreMediaData = true
|
||||
|
||||
camera.startVideoRecording(completion: { error in }, messengerForStreaming: nil)
|
||||
|
||||
let startWritingCalledBefore = startWritingCalled
|
||||
camera.captureOutput(
|
||||
camera.captureVideoOutput.avOutput,
|
||||
didOutput: videoSample,
|
||||
from: testVideoConnection)
|
||||
XCTAssert(
|
||||
(startWritingCalledBefore && videoAppended) || (startWritingCalled && !videoAppended),
|
||||
"The startWriting was called between sample creation and appending.")
|
||||
|
||||
camera.captureOutput(
|
||||
camera.captureVideoOutput.avOutput,
|
||||
didOutput: videoSample,
|
||||
from: testVideoConnection)
|
||||
XCTAssert(videoAppended, "Video was not appended.")
|
||||
}
|
||||
|
||||
func testStartVideoRecordingWithCompletionShouldNotDisableMixWithOthers() {
|
||||
let cam = CameraTestUtils.createCameraWithCaptureSessionQueue(DispatchQueue(label: "testing"))
|
||||
|
||||
try? AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers)
|
||||
cam.startVideoRecording(completion: { error in }, messengerForStreaming: nil)
|
||||
XCTAssert(
|
||||
AVAudioSession.sharedInstance().categoryOptions.contains(.mixWithOthers),
|
||||
"Flag MixWithOthers was removed.")
|
||||
XCTAssert(
|
||||
AVAudioSession.sharedInstance().category == .playAndRecord,
|
||||
"Category should be PlayAndRecord.")
|
||||
}
|
||||
|
||||
func testDidOutputSampleBufferMustUseSingleOffsetForVideoAndAudio() {
|
||||
let (camera, writerMock, adaptorMock, inputMock) = createCamera()
|
||||
|
||||
let testVideoConnection = CameraTestUtils.createTestConnection(
|
||||
camera.captureVideoOutput.avOutput)
|
||||
let testAudioOutput = CameraTestUtils.createTestAudioOutput()
|
||||
let testAudioConnection = CameraTestUtils.createTestConnection(testAudioOutput)
|
||||
|
||||
var status = AVAssetWriter.Status.unknown
|
||||
writerMock.startWritingStub = {
|
||||
status = .writing
|
||||
return true
|
||||
}
|
||||
writerMock.statusStub = {
|
||||
return status
|
||||
}
|
||||
|
||||
var appendedTime = CMTime.invalid
|
||||
|
||||
adaptorMock.appendStub = { buffer, time in
|
||||
appendedTime = time
|
||||
return true
|
||||
}
|
||||
|
||||
inputMock.isReadyForMoreMediaData = true
|
||||
inputMock.appendStub = { buffer in
|
||||
appendedTime = CMSampleBufferGetPresentationTimeStamp(buffer)
|
||||
return true
|
||||
}
|
||||
|
||||
camera.startVideoRecording(completion: { error in }, messengerForStreaming: nil)
|
||||
|
||||
let appendVideoSample = { (time: Int64) in
|
||||
camera.captureOutput(
|
||||
camera.captureVideoOutput.avOutput,
|
||||
didOutput: CameraTestUtils.createTestSampleBuffer(
|
||||
timestamp: CMTimeMake(value: time, timescale: 1),
|
||||
duration: .invalid),
|
||||
from: testVideoConnection)
|
||||
}
|
||||
|
||||
let appendAudioSample = { (time: Int64, duration: Int64) in
|
||||
camera.captureOutput(
|
||||
testAudioOutput,
|
||||
didOutput: CameraTestUtils.createTestAudioSampleBuffer(
|
||||
timestamp: CMTimeMake(value: time, timescale: 1),
|
||||
duration: CMTimeMake(value: duration, timescale: 1)),
|
||||
from: testAudioConnection)
|
||||
}
|
||||
|
||||
appendedTime = .invalid
|
||||
camera.pauseVideoRecording()
|
||||
camera.resumeVideoRecording()
|
||||
appendVideoSample(1)
|
||||
XCTAssertEqual(appendedTime, CMTimeMake(value: 1, timescale: 1))
|
||||
|
||||
appendedTime = .invalid
|
||||
camera.pauseVideoRecording()
|
||||
camera.resumeVideoRecording()
|
||||
appendVideoSample(11)
|
||||
XCTAssertEqual(appendedTime, .invalid)
|
||||
appendVideoSample(12)
|
||||
XCTAssertEqual(appendedTime, CMTimeMake(value: 2, timescale: 1))
|
||||
|
||||
appendedTime = .invalid
|
||||
camera.pauseVideoRecording()
|
||||
camera.resumeVideoRecording()
|
||||
appendAudioSample(20, 2)
|
||||
XCTAssertEqual(appendedTime, .invalid)
|
||||
appendVideoSample(23)
|
||||
XCTAssertEqual(appendedTime, CMTimeMake(value: 3, timescale: 1))
|
||||
|
||||
appendedTime = .invalid
|
||||
camera.pauseVideoRecording()
|
||||
camera.resumeVideoRecording()
|
||||
appendVideoSample(28)
|
||||
XCTAssertEqual(appendedTime, .invalid)
|
||||
appendAudioSample(30, 2)
|
||||
XCTAssertEqual(appendedTime, .invalid)
|
||||
appendVideoSample(33)
|
||||
XCTAssertEqual(appendedTime, .invalid)
|
||||
appendAudioSample(32, 2)
|
||||
XCTAssertEqual(appendedTime, CMTimeMake(value: 2, timescale: 1))
|
||||
}
|
||||
|
||||
func testDidOutputSampleBufferMustConnectVideoAfterSessionInterruption() {
|
||||
let (camera, writerMock, adaptorMock, inputMock) = createCamera()
|
||||
|
||||
let testVideoConnection = CameraTestUtils.createTestConnection(
|
||||
camera.captureVideoOutput.avOutput)
|
||||
let testAudioOutput = CameraTestUtils.createTestAudioOutput()
|
||||
let testAudioConnection = CameraTestUtils.createTestConnection(testAudioOutput)
|
||||
|
||||
var status = AVAssetWriter.Status.unknown
|
||||
writerMock.startWritingStub = {
|
||||
status = .writing
|
||||
return true
|
||||
}
|
||||
writerMock.statusStub = {
|
||||
return status
|
||||
}
|
||||
|
||||
var appendedTime = CMTime.invalid
|
||||
|
||||
adaptorMock.appendStub = { buffer, time in
|
||||
appendedTime = time
|
||||
return true
|
||||
}
|
||||
|
||||
inputMock.isReadyForMoreMediaData = true
|
||||
inputMock.appendStub = { buffer in
|
||||
appendedTime = CMSampleBufferGetPresentationTimeStamp(buffer)
|
||||
return true
|
||||
}
|
||||
|
||||
camera.startVideoRecording(completion: { error in }, messengerForStreaming: nil)
|
||||
|
||||
let appendVideoSample = { (time: Int64) in
|
||||
camera.captureOutput(
|
||||
camera.captureVideoOutput.avOutput,
|
||||
didOutput: CameraTestUtils.createTestSampleBuffer(
|
||||
timestamp: CMTimeMake(value: time, timescale: 1),
|
||||
duration: .invalid),
|
||||
from: testVideoConnection)
|
||||
}
|
||||
|
||||
let appendAudioSample = { (time: Int64, duration: Int64) in
|
||||
camera.captureOutput(
|
||||
testAudioOutput,
|
||||
didOutput: CameraTestUtils.createTestAudioSampleBuffer(
|
||||
timestamp: CMTimeMake(value: time, timescale: 1),
|
||||
duration: CMTimeMake(value: duration, timescale: 1)),
|
||||
from: testAudioConnection)
|
||||
}
|
||||
|
||||
appendVideoSample(1)
|
||||
appendAudioSample(1, 1)
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: AVCaptureSession.wasInterruptedNotification,
|
||||
object: camera.audioCaptureSession)
|
||||
|
||||
appendedTime = .invalid
|
||||
appendAudioSample(11, 1)
|
||||
XCTAssertEqual(appendedTime, .invalid)
|
||||
appendVideoSample(12)
|
||||
XCTAssertEqual(appendedTime, CMTimeMake(value: 2, timescale: 1))
|
||||
appendedTime = .invalid
|
||||
appendAudioSample(12, 1)
|
||||
XCTAssertEqual(appendedTime, CMTimeMake(value: 2, timescale: 1))
|
||||
}
|
||||
|
||||
func testStartVideoRecordingReportsErrorWhenStartWritingFails() {
|
||||
let (camera, writerMock, _, _) = createCamera()
|
||||
|
||||
// Configure the mock to return false for startWriting
|
||||
writerMock.startWritingStub = {
|
||||
return false
|
||||
}
|
||||
|
||||
// Configure a mock error
|
||||
let mockError = NSError(
|
||||
domain: "test", code: 123, userInfo: [NSLocalizedDescriptionKey: "Mock write error"])
|
||||
writerMock.error = mockError
|
||||
|
||||
let expectation = self.expectation(description: "Completion handler called with error")
|
||||
|
||||
camera.startVideoRecording(
|
||||
completion: { result in
|
||||
switch result {
|
||||
case .failure(let error as PigeonError):
|
||||
XCTAssertEqual(error.code, "IOError")
|
||||
XCTAssertEqual(error.message, "AVAssetWriter failed to start writing")
|
||||
XCTAssertEqual(error.details as? String, "Mock write error")
|
||||
XCTAssertFalse(camera.isRecording)
|
||||
expectation.fulfill()
|
||||
default:
|
||||
XCTFail("Expected PigeonError")
|
||||
}
|
||||
|
||||
}, messengerForStreaming: nil)
|
||||
|
||||
waitForExpectations(timeout: 1.0, handler: nil)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
final class SavePhotoDelegateTests: XCTestCase {
|
||||
func testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToCapture() {
|
||||
let completionExpectation = expectation(
|
||||
description: "Must complete with error if failed to capture photo.")
|
||||
let captureError = NSError(domain: "test", code: 0, userInfo: nil)
|
||||
let ioQueue = DispatchQueue(label: "test")
|
||||
let delegate = SavePhotoDelegate(path: "test", ioQueue: ioQueue) { path, error in
|
||||
XCTAssertEqual(captureError, error as NSError?)
|
||||
XCTAssertNil(path)
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
delegate.handlePhotoCaptureResult(error: captureError) { nil }
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToWrite() {
|
||||
let completionExpectation = expectation(
|
||||
description: "Must complete with error if failed to write file.")
|
||||
let ioQueue = DispatchQueue(label: "test")
|
||||
let ioError = NSError(
|
||||
domain: "IOError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Localized IO Error"])
|
||||
let delegate = SavePhotoDelegate(path: "test", ioQueue: ioQueue) { path, error in
|
||||
XCTAssertEqual(ioError, error as NSError?)
|
||||
XCTAssertNil(path)
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
let mockWritableData = MockWritableData()
|
||||
mockWritableData.writeToFileStub = { path, options in
|
||||
throw ioError
|
||||
}
|
||||
|
||||
delegate.handlePhotoCaptureResult(error: nil) { mockWritableData }
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testHandlePhotoCaptureResult_mustCompleteWithFilePathIfSuccessToWrite() {
|
||||
let completionExpectation = expectation(
|
||||
description: "Must complete with file path if succeeds to write file.")
|
||||
let ioQueue = DispatchQueue(label: "test")
|
||||
let filePath = "test"
|
||||
let delegate = SavePhotoDelegate(path: filePath, ioQueue: ioQueue) { path, error in
|
||||
XCTAssertNil(error)
|
||||
XCTAssertEqual(filePath, path)
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
let mockWritableData = MockWritableData()
|
||||
mockWritableData.writeToFileStub = { path, options in }
|
||||
|
||||
delegate.handlePhotoCaptureResult(error: nil) { mockWritableData }
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue() {
|
||||
let dataProviderQueueExpectation = expectation(
|
||||
description: "Data provider must run on io queue.")
|
||||
let writeFileQueueExpectation = expectation(description: "File writing must run on io queue.")
|
||||
let completionExpectation = expectation(
|
||||
description: "Must complete with file path if success to write file.")
|
||||
let ioQueue = DispatchQueue(label: "test")
|
||||
let ioQueueSpecific = DispatchSpecificKey<Void>()
|
||||
ioQueue.setSpecific(key: ioQueueSpecific, value: ())
|
||||
|
||||
let mockWritableData = MockWritableData()
|
||||
mockWritableData.writeToFileStub = { path, options in
|
||||
if DispatchQueue.getSpecific(key: ioQueueSpecific) != nil {
|
||||
writeFileQueueExpectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
let filePath = "test"
|
||||
let delegate = SavePhotoDelegate(path: filePath, ioQueue: ioQueue) { path, error in
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
|
||||
delegate.handlePhotoCaptureResult(error: nil) {
|
||||
if DispatchQueue.getSpecific(key: ioQueueSpecific) != nil {
|
||||
dataProviderQueueExpectation.fulfill()
|
||||
}
|
||||
return mockWritableData
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
}
|
||||
217
camera_avfoundation/darwin/Tests/StreamingTests.swift
Normal file
217
camera_avfoundation/darwin/Tests/StreamingTests.swift
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import Flutter
|
||||
import XCTest
|
||||
|
||||
@testable import camera_avfoundation
|
||||
|
||||
private class MockEventSink: PigeonEventSink<PlatformCameraImageData> {
|
||||
var eventSinkSuccessStub: ((PlatformCameraImageData?) -> Void)?
|
||||
|
||||
init() {
|
||||
super.init({ event in })
|
||||
}
|
||||
|
||||
override func success(_ event: PlatformCameraImageData?) {
|
||||
eventSinkSuccessStub?(event)
|
||||
}
|
||||
|
||||
override func error(code: String, message: String?, details: Any?) {}
|
||||
|
||||
override func endOfStream() {}
|
||||
}
|
||||
|
||||
private class MockImageStreamHandler: ImageDataStreamStreamHandler, ImageStreamHandler {
|
||||
var mockEventSink = MockEventSink()
|
||||
var captureSessionQueue: DispatchQueue {
|
||||
preconditionFailure("Attempted to access unimplemented property: captureSessionQueue")
|
||||
}
|
||||
var eventSink: PigeonEventSink<PlatformCameraImageData>? {
|
||||
set {
|
||||
}
|
||||
get {
|
||||
return mockEventSink
|
||||
}
|
||||
}
|
||||
|
||||
var eventSinkSuccessStub: ((PlatformCameraImageData?) -> Void)? {
|
||||
set {
|
||||
mockEventSink.eventSinkSuccessStub = newValue
|
||||
}
|
||||
get {
|
||||
return mockEventSink.eventSinkSuccessStub
|
||||
}
|
||||
}
|
||||
|
||||
override func onListen(
|
||||
withArguments arguments: Any?, sink: PigeonEventSink<PlatformCameraImageData>
|
||||
) {
|
||||
}
|
||||
|
||||
override func onCancel(withArguments arguments: Any?) {
|
||||
}
|
||||
}
|
||||
|
||||
final class StreamingTests: XCTestCase {
|
||||
private func createCamera() -> (
|
||||
DefaultCamera,
|
||||
AVCaptureOutput,
|
||||
CMSampleBuffer,
|
||||
CMSampleBuffer,
|
||||
AVCaptureConnection
|
||||
) {
|
||||
let captureSessionQueue = DispatchQueue(label: "testing")
|
||||
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
||||
configuration.captureSessionQueue = captureSessionQueue
|
||||
|
||||
let camera = CameraTestUtils.createTestCamera(configuration)
|
||||
let testAudioOutput = CameraTestUtils.createTestAudioOutput()
|
||||
let sampleBuffer = CameraTestUtils.createTestSampleBuffer()
|
||||
let audioSampleBuffer = CameraTestUtils.createTestAudioSampleBuffer()
|
||||
let testAudioConnection = CameraTestUtils.createTestConnection(testAudioOutput)
|
||||
|
||||
return (camera, testAudioOutput, sampleBuffer, audioSampleBuffer, testAudioConnection)
|
||||
}
|
||||
|
||||
func testExceedMaxStreamingPendingFramesCount() {
|
||||
let (camera, testAudioOutput, sampleBuffer, _, testAudioConnection) = createCamera()
|
||||
let handlerMock = MockImageStreamHandler()
|
||||
|
||||
let finishStartStreamExpectation = expectation(
|
||||
description: "Finish startStream")
|
||||
|
||||
let messenger = MockFlutterBinaryMessenger()
|
||||
camera.startImageStream(
|
||||
with: messenger, imageStreamHandler: handlerMock,
|
||||
completion: {
|
||||
_ in
|
||||
finishStartStreamExpectation.fulfill()
|
||||
})
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
// Setup mocked event sink after the stream starts
|
||||
let streamingExpectation = expectation(
|
||||
description: "Must not call handler over maxStreamingPendingFramesCount")
|
||||
|
||||
handlerMock.eventSinkSuccessStub = { event in
|
||||
streamingExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForQueueRoundTrip(with: DispatchQueue.main)
|
||||
XCTAssertEqual(camera.isStreamingImages, true)
|
||||
|
||||
streamingExpectation.expectedFulfillmentCount = 4
|
||||
for _ in 0..<10 {
|
||||
camera.captureOutput(testAudioOutput, didOutput: sampleBuffer, from: testAudioConnection)
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testReceivedImageStreamData() {
|
||||
let (camera, testAudioOutput, sampleBuffer, _, testAudioConnection) = createCamera()
|
||||
let handlerMock = MockImageStreamHandler()
|
||||
|
||||
let finishStartStreamExpectation = expectation(
|
||||
description: "Finish startStream")
|
||||
|
||||
let messenger = MockFlutterBinaryMessenger()
|
||||
camera.startImageStream(
|
||||
with: messenger, imageStreamHandler: handlerMock,
|
||||
completion: {
|
||||
_ in
|
||||
finishStartStreamExpectation.fulfill()
|
||||
})
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
|
||||
// Setup mocked event sink after the stream starts
|
||||
let streamingExpectation = expectation(
|
||||
description: "Must be able to call the handler again when receivedImageStreamData is called")
|
||||
handlerMock.eventSinkSuccessStub = { event in
|
||||
streamingExpectation.fulfill()
|
||||
}
|
||||
|
||||
waitForQueueRoundTrip(with: DispatchQueue.main)
|
||||
XCTAssertEqual(camera.isStreamingImages, true)
|
||||
|
||||
streamingExpectation.expectedFulfillmentCount = 5
|
||||
for _ in 0..<10 {
|
||||
camera.captureOutput(testAudioOutput, didOutput: sampleBuffer, from: testAudioConnection)
|
||||
}
|
||||
|
||||
camera.receivedImageStreamData()
|
||||
camera.captureOutput(testAudioOutput, didOutput: sampleBuffer, from: testAudioConnection)
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
|
||||
func testIgnoresNonImageBuffers() {
|
||||
let (camera, testAudioOutput, _, audioSampleBuffer, testAudioConnection) = createCamera()
|
||||
let handlerMock = MockImageStreamHandler()
|
||||
handlerMock.eventSinkSuccessStub = { event in
|
||||
XCTFail()
|
||||
}
|
||||
|
||||
let finishStartStreamExpectation = expectation(
|
||||
description: "Finish startStream")
|
||||
|
||||
let messenger = MockFlutterBinaryMessenger()
|
||||
camera.startImageStream(
|
||||
with: messenger, imageStreamHandler: handlerMock,
|
||||
completion: {
|
||||
_ in
|
||||
finishStartStreamExpectation.fulfill()
|
||||
})
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
XCTAssertEqual(camera.isStreamingImages, true)
|
||||
|
||||
camera.captureOutput(testAudioOutput, didOutput: audioSampleBuffer, from: testAudioConnection)
|
||||
|
||||
waitForQueueRoundTrip(with: DispatchQueue.main)
|
||||
}
|
||||
|
||||
func testImageStreamEventFormat() throws {
|
||||
let (camera, testAudioOutput, sampleBuffer, _, testAudioConnection) = createCamera()
|
||||
|
||||
let expectation = expectation(description: "Received a valid event")
|
||||
|
||||
let handlerMock = MockImageStreamHandler()
|
||||
handlerMock.eventSinkSuccessStub = { event in
|
||||
guard let imageBuffer = event else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
XCTAssertGreaterThan(imageBuffer.width, 0)
|
||||
XCTAssertGreaterThan(imageBuffer.height, 0)
|
||||
XCTAssertGreaterThan(imageBuffer.formatCode, 0)
|
||||
XCTAssertGreaterThan(imageBuffer.lensAperture, 0)
|
||||
XCTAssertGreaterThan(imageBuffer.sensorExposureTimeNanoseconds, 0)
|
||||
XCTAssertGreaterThan(imageBuffer.sensorSensitivity, 0)
|
||||
|
||||
let planes = imageBuffer.planes
|
||||
let planeBuffer = planes[0]
|
||||
|
||||
XCTAssertGreaterThan(planeBuffer.bytesPerRow, 0)
|
||||
XCTAssertGreaterThan(planeBuffer.width, 0)
|
||||
XCTAssertGreaterThan(planeBuffer.height, 0)
|
||||
XCTAssertGreaterThan(planeBuffer.bytes.data.count, 0)
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
let messenger = MockFlutterBinaryMessenger()
|
||||
camera.startImageStream(with: messenger, imageStreamHandler: handlerMock) { _ in }
|
||||
|
||||
waitForQueueRoundTrip(with: DispatchQueue.main)
|
||||
XCTAssertEqual(camera.isStreamingImages, true)
|
||||
|
||||
camera.captureOutput(testAudioOutput, didOutput: sampleBuffer, from: testAudioConnection)
|
||||
|
||||
waitForExpectations(timeout: 30, handler: nil)
|
||||
}
|
||||
}
|
||||
27
camera_avfoundation/darwin/camera_avfoundation.podspec
Normal file
27
camera_avfoundation/darwin/camera_avfoundation.podspec
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
|
||||
#
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'camera_avfoundation'
|
||||
s.version = '0.0.1'
|
||||
s.summary = 'Flutter Camera'
|
||||
s.description = <<-DESC
|
||||
A Flutter plugin to use the camera from your Flutter app.
|
||||
DESC
|
||||
s.homepage = 'https://github.com/flutter/packages'
|
||||
s.license = { :type => 'BSD', :file => '../LICENSE' }
|
||||
s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
|
||||
s.source = { :http => 'https://github.com/flutter/packages/tree/main/packages/camera_avfoundation' }
|
||||
s.documentation_url = 'https://pub.dev/packages/camera_avfoundation'
|
||||
s.source_files = 'camera_avfoundation/Sources/camera_avfoundation/**/*.swift'
|
||||
s.swift_version = '5.0'
|
||||
s.xcconfig = {
|
||||
'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift',
|
||||
'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift',
|
||||
}
|
||||
s.dependency 'Flutter'
|
||||
|
||||
s.platform = :ios, '13.0'
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
||||
s.resource_bundles = {'camera_avfoundation_privacy' => ['camera_avfoundation/Sources/camera_avfoundation/Resources/PrivacyInfo.xcprivacy']}
|
||||
end
|
||||
28
camera_avfoundation/darwin/camera_avfoundation/Package.swift
Normal file
28
camera_avfoundation/darwin/camera_avfoundation/Package.swift
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// swift-tools-version: 5.9
|
||||
|
||||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "camera_avfoundation",
|
||||
platforms: [
|
||||
.iOS("13.0")
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "camera-avfoundation", targets: ["camera_avfoundation"])
|
||||
],
|
||||
dependencies: [],
|
||||
targets: [
|
||||
.target(
|
||||
name: "camera_avfoundation",
|
||||
path: "Sources/camera_avfoundation",
|
||||
resources: [
|
||||
.process("Resources")
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
|
||||
/// A protocol that is a direct passthrough to `AVAssetWriter`. It is used to allow for mocking
|
||||
/// `AVAssetWriter` in tests.
|
||||
protocol AssetWriter: NSObjectProtocol {
|
||||
var status: AVAssetWriter.Status { get }
|
||||
var error: Error? { get }
|
||||
|
||||
func startWriting() -> Bool
|
||||
func finishWriting(completionHandler handler: @escaping @Sendable () -> Void)
|
||||
func startSession(atSourceTime startTime: CMTime)
|
||||
func add(_ input: AVAssetWriterInput)
|
||||
}
|
||||
|
||||
/// A protocol that is a direct passthrough to `AVAssetWriterInput`. It is used to allow for mocking
|
||||
/// `AVAssetWriterInput` in tests.
|
||||
protocol AssetWriterInput: NSObjectProtocol {
|
||||
/// The underlying `AVAssetWriterInput` instance. This exists so that the input
|
||||
/// can be extracted when adding to an AVAssetWriter.
|
||||
var avInput: AVAssetWriterInput { get }
|
||||
|
||||
var expectsMediaDataInRealTime: Bool { get set }
|
||||
var isReadyForMoreMediaData: Bool { get }
|
||||
|
||||
func append(_ sampleBuffer: CMSampleBuffer) -> Bool
|
||||
}
|
||||
|
||||
/// A protocol that is a direct passthrough to `AVAssetWriterInputPixelBufferAdaptor`. It is used to
|
||||
/// allow for mocking `AVAssetWriterInputPixelBufferAdaptor` in tests.
|
||||
protocol AssetWriterInputPixelBufferAdaptor: NSObjectProtocol {
|
||||
func append(_ pixelBuffer: CVPixelBuffer, withPresentationTime presentationTime: CMTime) -> Bool
|
||||
}
|
||||
|
||||
extension AVAssetWriter: AssetWriter {}
|
||||
|
||||
extension AVAssetWriterInput: AssetWriterInput {
|
||||
var avInput: AVAssetWriterInput { self }
|
||||
}
|
||||
|
||||
extension AVAssetWriterInputPixelBufferAdaptor: AssetWriterInputPixelBufferAdaptor {}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import CoreMotion
|
||||
import Flutter
|
||||
|
||||
/// A class that manages camera's state and performs camera operations.
|
||||
protocol Camera: FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate,
|
||||
AVCaptureAudioDataOutputSampleBufferDelegate
|
||||
{
|
||||
/// The API instance used to communicate with the Dart side of the plugin.
|
||||
/// Once initially set, this should only ever be accessed on the main thread.
|
||||
var dartAPI: CameraEventApi? { get set }
|
||||
|
||||
var onFrameAvailable: (() -> Void)? { get set }
|
||||
|
||||
/// Format used for video and image streaming.
|
||||
var videoFormat: FourCharCode { get set }
|
||||
|
||||
var isPreviewPaused: Bool { get }
|
||||
var isStreamingImages: Bool { get }
|
||||
|
||||
var deviceOrientation: UIDeviceOrientation { get set }
|
||||
|
||||
var minimumAvailableZoomFactor: CGFloat { get }
|
||||
var maximumAvailableZoomFactor: CGFloat { get }
|
||||
var minimumExposureOffset: CGFloat { get }
|
||||
var maximumExposureOffset: CGFloat { get }
|
||||
|
||||
func setUpCaptureSessionForAudioIfNeeded()
|
||||
|
||||
/// Informs the Dart side of the plugin of the current camera state and capabilities.
|
||||
func reportInitializationState()
|
||||
|
||||
/// Acknowledges the receipt of one image stream frame.
|
||||
///
|
||||
/// This should be called each time a frame is received. Failing to call it may
|
||||
/// cause later frames to be dropped instead of streamed.
|
||||
func receivedImageStreamData()
|
||||
|
||||
func start()
|
||||
func stop()
|
||||
|
||||
/// Starts recording a video with an optional streaming messenger.
|
||||
/// If the messenger is non-nil then it will be called for each
|
||||
/// captured frame, allowing streaming concurrently with recording.
|
||||
///
|
||||
/// @param messenger Nullable messenger for capturing each frame.
|
||||
func startVideoRecording(
|
||||
completion: @escaping (Result<Void, any Error>) -> Void,
|
||||
messengerForStreaming: FlutterBinaryMessenger?
|
||||
)
|
||||
func pauseVideoRecording()
|
||||
func resumeVideoRecording()
|
||||
func stopVideoRecording(completion: @escaping (Result<String, any Error>) -> Void)
|
||||
|
||||
func captureToFile(completion: @escaping (Result<String, any Error>) -> Void)
|
||||
|
||||
func lockCaptureOrientation(_ orientation: PlatformDeviceOrientation)
|
||||
func unlockCaptureOrientation()
|
||||
|
||||
func setImageFileFormat(_ fileFormat: PlatformImageFileFormat)
|
||||
func setJpegImageQuality(_ quality: Int64)
|
||||
|
||||
func setExposureMode(_ mode: PlatformExposureMode)
|
||||
func setExposureOffset(_ offset: Double)
|
||||
|
||||
/// Sets the exposure point, in a (0,1) coordinate system.
|
||||
///
|
||||
/// If @c point is nil, the exposure point will reset to the center.
|
||||
func setExposurePoint(
|
||||
_ point: PlatformPoint?,
|
||||
withCompletion: @escaping (Result<Void, any Error>) -> Void
|
||||
)
|
||||
|
||||
/// Sets FocusMode on the current AVCaptureDevice.
|
||||
///
|
||||
/// If the @c focusMode is set to FocusModeAuto the AVCaptureDevice is configured to use
|
||||
/// AVCaptureFocusModeContinuousModeAutoFocus when supported, otherwise it is set to
|
||||
/// AVCaptureFocusModeAutoFocus. If neither AVCaptureFocusModeContinuousModeAutoFocus nor
|
||||
/// AVCaptureFocusModeAutoFocus are supported focus mode will not be set.
|
||||
/// If @c focusMode is set to FocusModeLocked the AVCaptureDevice is configured to use
|
||||
/// AVCaptureFocusModeAutoFocus. If AVCaptureFocusModeAutoFocus is not supported focus mode will not
|
||||
/// be set.
|
||||
///
|
||||
/// @param mode The focus mode that should be applied.
|
||||
func setFocusMode(_ mode: PlatformFocusMode)
|
||||
|
||||
/// Sets the focus point, in a (0,1) coordinate system.
|
||||
///
|
||||
/// If @c point is nil, the focus point will reset to the center.
|
||||
func setFocusPoint(
|
||||
_ point: PlatformPoint?,
|
||||
completion: @escaping (Result<Void, any Error>) -> Void
|
||||
)
|
||||
|
||||
func setZoomLevel(_ zoom: CGFloat, withCompletion: @escaping (Result<Void, any Error>) -> Void)
|
||||
|
||||
func setVideoStabilizationMode(
|
||||
_ mode: PlatformVideoStabilizationMode,
|
||||
withCompletion: @escaping (Result<Void, any Error>) -> Void)
|
||||
|
||||
func isVideoStabilizationModeSupported(_ mode: PlatformVideoStabilizationMode) -> Bool
|
||||
|
||||
func setFlashMode(
|
||||
_ mode: PlatformFlashMode,
|
||||
withCompletion: @escaping (Result<Void, any Error>) -> Void
|
||||
)
|
||||
|
||||
func pausePreview()
|
||||
func resumePreview()
|
||||
|
||||
func setDescriptionWhileRecording(
|
||||
_ cameraName: String,
|
||||
withCompletion: @escaping (Result<Void, any Error>) -> Void
|
||||
)
|
||||
|
||||
func startImageStream(
|
||||
with: FlutterBinaryMessenger, completion: @escaping (Result<Void, any Error>) -> Void)
|
||||
func stopImageStream()
|
||||
|
||||
// Override to make `AVCaptureVideoDataOutputSampleBufferDelegate`/
|
||||
// `AVCaptureAudioDataOutputSampleBufferDelegate` method non optional
|
||||
override func captureOutput(
|
||||
_ output: AVCaptureOutput,
|
||||
didOutput sampleBuffer: CMSampleBuffer,
|
||||
from connection: AVCaptureConnection
|
||||
)
|
||||
|
||||
func close()
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import CoreMedia
|
||||
import UIKit
|
||||
|
||||
/// Factory block returning an FLTCaptureDevice.
|
||||
/// Used in tests to inject a video capture device into DefaultCamera.
|
||||
typealias VideoCaptureDeviceFactory = (_ cameraName: String) -> CaptureDevice
|
||||
|
||||
typealias AudioCaptureDeviceFactory = () -> CaptureDevice
|
||||
|
||||
typealias CaptureSessionFactory = () -> CaptureSession
|
||||
|
||||
typealias AssetWriterFactory = (_ assetUrl: URL, _ fileType: AVFileType) throws -> AssetWriter
|
||||
|
||||
typealias InputPixelBufferAdaptorFactory = (
|
||||
_ input: AssetWriterInput, _ settings: [String: Any]?
|
||||
) ->
|
||||
AssetWriterInputPixelBufferAdaptor
|
||||
|
||||
/// A configuration object that centralizes dependencies for `DefaultCamera`.
|
||||
class CameraConfiguration {
|
||||
var mediaSettings: PlatformMediaSettings
|
||||
var mediaSettingsWrapper: FLTCamMediaSettingsAVWrapper
|
||||
var captureSessionQueue: DispatchQueue
|
||||
var videoCaptureSession: CaptureSession
|
||||
var audioCaptureSession: CaptureSession
|
||||
var videoCaptureDeviceFactory: VideoCaptureDeviceFactory
|
||||
let audioCaptureDeviceFactory: AudioCaptureDeviceFactory
|
||||
let captureDeviceInputFactory: CaptureDeviceInputFactory
|
||||
var assetWriterFactory: AssetWriterFactory
|
||||
var inputPixelBufferAdaptorFactory: InputPixelBufferAdaptorFactory
|
||||
var videoDimensionsConverter: VideoDimensionsConverter
|
||||
var deviceOrientationProvider: DeviceOrientationProvider
|
||||
let initialCameraName: String
|
||||
var orientation: UIDeviceOrientation
|
||||
|
||||
init(
|
||||
mediaSettings: PlatformMediaSettings,
|
||||
mediaSettingsWrapper: FLTCamMediaSettingsAVWrapper,
|
||||
captureDeviceFactory: @escaping VideoCaptureDeviceFactory,
|
||||
audioCaptureDeviceFactory: @escaping AudioCaptureDeviceFactory,
|
||||
captureSessionFactory: @escaping CaptureSessionFactory,
|
||||
captureSessionQueue: DispatchQueue,
|
||||
captureDeviceInputFactory: CaptureDeviceInputFactory,
|
||||
initialCameraName: String
|
||||
) {
|
||||
self.mediaSettings = mediaSettings
|
||||
self.mediaSettingsWrapper = mediaSettingsWrapper
|
||||
self.videoCaptureDeviceFactory = captureDeviceFactory
|
||||
self.audioCaptureDeviceFactory = audioCaptureDeviceFactory
|
||||
self.captureSessionQueue = captureSessionQueue
|
||||
self.videoCaptureSession = captureSessionFactory()
|
||||
self.audioCaptureSession = captureSessionFactory()
|
||||
self.captureDeviceInputFactory = captureDeviceInputFactory
|
||||
self.initialCameraName = initialCameraName
|
||||
self.orientation = UIDevice.current.orientation
|
||||
self.deviceOrientationProvider = DefaultDeviceOrientationProvider()
|
||||
|
||||
self.videoDimensionsConverter = { format in
|
||||
return CMVideoFormatDescriptionGetDimensions(format.formatDescription)
|
||||
}
|
||||
|
||||
self.assetWriterFactory = { url, fileType in
|
||||
return try AVAssetWriter(outputURL: url, fileType: fileType)
|
||||
}
|
||||
|
||||
self.inputPixelBufferAdaptorFactory = { assetWriterInput, sourcePixelBufferAttributes in
|
||||
return AVAssetWriterInputPixelBufferAdaptor(
|
||||
assetWriterInput: assetWriterInput.avInput,
|
||||
sourcePixelBufferAttributes: sourcePixelBufferAttributes
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
/// A protocol which abstracts the discovery of camera devices.
|
||||
/// It is a thin wrapper around `AVCaptureDiscoverySession` and it exists to allow mocking in tests.
|
||||
protocol CameraDeviceDiscoverer {
|
||||
func discoverySession(
|
||||
withDeviceTypes deviceTypes: [AVCaptureDevice.DeviceType],
|
||||
mediaType: AVMediaType,
|
||||
position: AVCaptureDevice.Position
|
||||
) -> [CaptureDevice]
|
||||
}
|
||||
|
||||
/// The default implementation of the `CameraDeviceDiscoverer` protocol.
|
||||
/// It wraps a call to `AVCaptureDeviceDiscoverySession`.
|
||||
class DefaultCameraDeviceDiscoverer: NSObject, CameraDeviceDiscoverer {
|
||||
func discoverySession(
|
||||
withDeviceTypes deviceTypes: [AVCaptureDevice.DeviceType],
|
||||
mediaType: AVMediaType,
|
||||
position: AVCaptureDevice.Position
|
||||
) -> [CaptureDevice] {
|
||||
return AVCaptureDevice.DiscoverySession(
|
||||
deviceTypes: deviceTypes,
|
||||
mediaType: mediaType,
|
||||
position: position
|
||||
).devices
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2026 twonly contributors. BSD-style license; see LICENSE.
|
||||
|
||||
import Accelerate
|
||||
import Flutter
|
||||
|
||||
/// Reduces BGRA analysis frames before they cross the Flutter bridge. The
|
||||
/// original pixel buffer remains available to the preview and video recorder.
|
||||
enum CameraImageStreamScaler {
|
||||
// The caller holds the pixel buffer's read lock for the duration of this call.
|
||||
static func scaleBGRA(_ pixelBuffer: CVPixelBuffer, maximumDimension: Int)
|
||||
-> PlatformCameraImagePlane?
|
||||
{
|
||||
guard CVPixelBufferGetPixelFormatType(pixelBuffer) == kCVPixelFormatType_32BGRA,
|
||||
maximumDimension > 0,
|
||||
let address = CVPixelBufferGetBaseAddress(pixelBuffer)
|
||||
else { return nil }
|
||||
|
||||
let width = CVPixelBufferGetWidth(pixelBuffer)
|
||||
let height = CVPixelBufferGetHeight(pixelBuffer)
|
||||
let longest = max(width, height)
|
||||
guard longest > maximumDimension else { return nil }
|
||||
let scale = Double(maximumDimension) / Double(longest)
|
||||
let targetWidth = max(1, Int((Double(width) * scale).rounded()))
|
||||
let targetHeight = max(1, Int((Double(height) * scale).rounded()))
|
||||
let stride = targetWidth * 4
|
||||
var bytes = Data(count: stride * targetHeight)
|
||||
var source = vImage_Buffer(
|
||||
data: address, height: vImagePixelCount(height), width: vImagePixelCount(width),
|
||||
rowBytes: CVPixelBufferGetBytesPerRow(pixelBuffer))
|
||||
let error = bytes.withUnsafeMutableBytes { buffer -> vImage_Error in
|
||||
var destination = vImage_Buffer(
|
||||
data: buffer.baseAddress!, height: vImagePixelCount(targetHeight),
|
||||
width: vImagePixelCount(targetWidth), rowBytes: stride)
|
||||
return vImageScale_ARGB8888(&source, &destination, nil, vImage_Flags(kvImageNoFlags))
|
||||
}
|
||||
guard error == kvImageNoError else { return nil }
|
||||
return PlatformCameraImagePlane(
|
||||
bytes: FlutterStandardTypedData(bytes: bytes), bytesPerRow: Int64(stride),
|
||||
width: Int64(targetWidth), height: Int64(targetHeight))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import Flutter
|
||||
|
||||
/// Completion handler for camera permission requests.
|
||||
typealias CameraPermissionRequestCompletionHandler = (PigeonError?) -> Void
|
||||
|
||||
private enum Permission {
|
||||
case camera
|
||||
case audio
|
||||
}
|
||||
|
||||
/// Manages camera and audio permission requests.
|
||||
class CameraPermissionManager: NSObject {
|
||||
let permissionService: PermissionServicing
|
||||
|
||||
init(permissionService: PermissionServicing) {
|
||||
self.permissionService = permissionService
|
||||
super.init()
|
||||
}
|
||||
|
||||
/// Requests camera access permission.
|
||||
///
|
||||
/// If it is the first time requesting camera access, a permission dialog will show up on the
|
||||
/// screen. Otherwise AVFoundation simply returns the user's previous choice, and in this case the
|
||||
/// user will have to update the choice in Settings app.
|
||||
///
|
||||
/// @param handler if access permission is (or was previously) granted, completion handler will be
|
||||
/// called without error; Otherwise completion handler will be called with error. Handler can be
|
||||
/// called on an arbitrary dispatch queue.
|
||||
func requestCameraPermission(
|
||||
completionHandler handler: @escaping CameraPermissionRequestCompletionHandler
|
||||
) {
|
||||
requestPermission(permission: .camera, handler: handler)
|
||||
}
|
||||
|
||||
/// Requests audio access permission.
|
||||
///
|
||||
/// If it is the first time requesting audio access, a permission dialog will show up on the
|
||||
/// screen. Otherwise AVFoundation simply returns the user's previous choice, and in this case the
|
||||
/// user will have to update the choice in Settings app.
|
||||
///
|
||||
/// @param handler if access permission is (or was previously) granted, completion handler will be
|
||||
/// called without error; Otherwise completion handler will be called with error. Handler can be
|
||||
/// called on an arbitrary dispatch queue.
|
||||
func requestAudioPermission(
|
||||
completionHandler handler: @escaping CameraPermissionRequestCompletionHandler
|
||||
) {
|
||||
requestPermission(permission: .audio, handler: handler)
|
||||
}
|
||||
|
||||
private func requestPermission(
|
||||
permission: Permission,
|
||||
handler: @escaping CameraPermissionRequestCompletionHandler
|
||||
) {
|
||||
let mediaType: AVMediaType =
|
||||
switch permission {
|
||||
case .audio: .audio
|
||||
case .camera: .video
|
||||
}
|
||||
|
||||
switch permissionService.authorizationStatus(for: mediaType) {
|
||||
case .authorized:
|
||||
handler(nil)
|
||||
|
||||
case .denied:
|
||||
let flutterError =
|
||||
switch permission {
|
||||
case .audio:
|
||||
PigeonError(
|
||||
code: "AudioAccessDeniedWithoutPrompt",
|
||||
message:
|
||||
"User has previously denied the audio access request. Go to Settings to enable audio access.",
|
||||
details: nil
|
||||
)
|
||||
case .camera:
|
||||
PigeonError(
|
||||
code: "CameraAccessDeniedWithoutPrompt",
|
||||
message:
|
||||
"User has previously denied the camera access request. Go to Settings to enable camera access.",
|
||||
details: nil
|
||||
)
|
||||
}
|
||||
handler(flutterError)
|
||||
|
||||
case .restricted:
|
||||
let flutterError =
|
||||
switch permission {
|
||||
case .audio:
|
||||
PigeonError(
|
||||
code: "AudioAccessRestricted",
|
||||
message: "Audio access is restricted.",
|
||||
details: nil
|
||||
)
|
||||
case .camera:
|
||||
PigeonError(
|
||||
code: "CameraAccessRestricted",
|
||||
message: "Camera access is restricted.",
|
||||
details: nil
|
||||
)
|
||||
}
|
||||
handler(flutterError)
|
||||
|
||||
case .notDetermined:
|
||||
permissionService.requestAccess(for: mediaType) { granted in
|
||||
// handler can be invoked on an arbitrary dispatch queue.
|
||||
if granted {
|
||||
handler(nil)
|
||||
} else {
|
||||
let flutterError =
|
||||
switch permission {
|
||||
case .audio:
|
||||
PigeonError(
|
||||
code: "AudioAccessDenied",
|
||||
message: "User denied the audio access request.",
|
||||
details: nil
|
||||
)
|
||||
case .camera:
|
||||
PigeonError(
|
||||
code: "CameraAccessDenied",
|
||||
message: "User denied the camera access request.",
|
||||
details: nil
|
||||
)
|
||||
}
|
||||
handler(flutterError)
|
||||
}
|
||||
}
|
||||
|
||||
@unknown default:
|
||||
assertionFailure("Unknown authorization status")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,567 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import Flutter
|
||||
|
||||
public final class CameraPlugin: NSObject, FlutterPlugin {
|
||||
private let registry: FlutterTextureRegistry
|
||||
private let messenger: FlutterBinaryMessenger
|
||||
private let globalEventAPI: CameraGlobalEventApiProtocol
|
||||
private let deviceDiscoverer: CameraDeviceDiscoverer
|
||||
private let permissionManager: CameraPermissionManager
|
||||
private let captureDeviceFactory: VideoCaptureDeviceFactory
|
||||
private let captureSessionFactory: CaptureSessionFactory
|
||||
private let captureDeviceInputFactory: CaptureDeviceInputFactory
|
||||
|
||||
/// All FLTCam's state access and capture session related operations should be on run on this queue.
|
||||
private let captureSessionQueue: DispatchQueue
|
||||
|
||||
/// An internal camera object that manages camera's state and performs camera operations.
|
||||
var camera: Camera?
|
||||
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let instance = CameraPlugin(
|
||||
registry: registrar.textures(),
|
||||
messenger: registrar.messenger(),
|
||||
globalAPI: CameraGlobalEventApi(binaryMessenger: registrar.messenger()),
|
||||
deviceDiscoverer: DefaultCameraDeviceDiscoverer(),
|
||||
permissionManager: CameraPermissionManager(
|
||||
permissionService: DefaultPermissionService()),
|
||||
deviceFactory: { name in
|
||||
// TODO(RobertOdrowaz) Implement better error handling and remove non-null assertion
|
||||
AVCaptureDevice(uniqueID: name)!
|
||||
},
|
||||
captureSessionFactory: { AVCaptureSession() },
|
||||
captureDeviceInputFactory: DefaultCaptureDeviceInputFactory(),
|
||||
captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue")
|
||||
)
|
||||
|
||||
CameraApiSetup.setUp(binaryMessenger: registrar.messenger(), api: instance)
|
||||
}
|
||||
|
||||
init(
|
||||
registry: FlutterTextureRegistry,
|
||||
messenger: FlutterBinaryMessenger,
|
||||
globalAPI: CameraGlobalEventApiProtocol,
|
||||
deviceDiscoverer: CameraDeviceDiscoverer,
|
||||
permissionManager: CameraPermissionManager,
|
||||
deviceFactory: @escaping VideoCaptureDeviceFactory,
|
||||
captureSessionFactory: @escaping CaptureSessionFactory,
|
||||
captureDeviceInputFactory: CaptureDeviceInputFactory,
|
||||
captureSessionQueue: DispatchQueue
|
||||
) {
|
||||
self.registry = registry
|
||||
self.messenger = messenger
|
||||
self.globalEventAPI = globalAPI
|
||||
self.deviceDiscoverer = deviceDiscoverer
|
||||
self.permissionManager = permissionManager
|
||||
self.captureDeviceFactory = deviceFactory
|
||||
self.captureSessionFactory = captureSessionFactory
|
||||
self.captureDeviceInputFactory = captureDeviceInputFactory
|
||||
self.captureSessionQueue = captureSessionQueue
|
||||
|
||||
super.init()
|
||||
|
||||
captureSessionQueue.setSpecific(
|
||||
key: captureSessionQueueSpecificKey, value: captureSessionQueueSpecificValue)
|
||||
|
||||
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: UIDevice.orientationDidChangeNotification,
|
||||
object: UIDevice.current,
|
||||
queue: .main
|
||||
) { [weak self] notification in
|
||||
self?.orientationChanged(notification)
|
||||
}
|
||||
}
|
||||
|
||||
public func detachFromEngine(for registrar: FlutterPluginRegistrar) {
|
||||
UIDevice.current.endGeneratingDeviceOrientationNotifications()
|
||||
}
|
||||
|
||||
private static func pigeonErrorFromNSError(_ error: NSError) -> PigeonError {
|
||||
return PigeonError(
|
||||
code: "Error \(error.code)",
|
||||
message: error.localizedDescription,
|
||||
details: error.domain)
|
||||
}
|
||||
|
||||
func orientationChanged(_ notification: Notification) {
|
||||
guard let device = notification.object as? UIDevice else { return }
|
||||
let orientation = device.orientation
|
||||
|
||||
if orientation == .faceUp || orientation == .faceDown {
|
||||
// Do not change when oriented flat.
|
||||
return
|
||||
}
|
||||
|
||||
self.captureSessionQueue.async { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
// `Camera.deviceOrientation` must be set on capture session queue.
|
||||
strongSelf.camera?.deviceOrientation = orientation
|
||||
// `CameraPlugin.sendDeviceOrientation` can be called on any queue.
|
||||
strongSelf.sendDeviceOrientation(orientation)
|
||||
}
|
||||
}
|
||||
|
||||
func sendDeviceOrientation(_ orientation: UIDeviceOrientation) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.globalEventAPI.deviceOrientationChanged(
|
||||
orientation: getPigeonDeviceOrientation(for: orientation)
|
||||
) { _ in
|
||||
// Ignore errors; this is essentially a broadcast stream, and
|
||||
// it's fine if the other end doesn't receive the message
|
||||
// (e.g., if it doesn't currently have a listener set up).
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CameraPlugin: CameraApi {
|
||||
|
||||
func getAvailableCameras(
|
||||
completion: @escaping (Result<[PlatformCameraDescription], any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
let discoveryDevices: [AVCaptureDevice.DeviceType] = [
|
||||
.builtInWideAngleCamera,
|
||||
.builtInTelephotoCamera,
|
||||
.builtInUltraWideCamera,
|
||||
]
|
||||
|
||||
let devices = strongSelf.deviceDiscoverer.discoverySession(
|
||||
withDeviceTypes: discoveryDevices,
|
||||
mediaType: .video,
|
||||
position: .unspecified)
|
||||
|
||||
var reply: [PlatformCameraDescription] = []
|
||||
|
||||
for device in devices {
|
||||
let lensFacing = strongSelf.platformLensDirection(for: device)
|
||||
let lensType = strongSelf.platformLensType(for: device)
|
||||
let cameraDescription = PlatformCameraDescription(
|
||||
name: device.uniqueID,
|
||||
lensDirection: lensFacing,
|
||||
lensType: lensType
|
||||
)
|
||||
reply.append(cameraDescription)
|
||||
}
|
||||
|
||||
completion(.success(reply))
|
||||
}
|
||||
}
|
||||
|
||||
private func platformLensDirection(for device: CaptureDevice) -> PlatformCameraLensDirection {
|
||||
switch device.position {
|
||||
case .back:
|
||||
return .back
|
||||
case .front:
|
||||
return .front
|
||||
case .unspecified:
|
||||
return .external
|
||||
@unknown default:
|
||||
return .external
|
||||
}
|
||||
}
|
||||
|
||||
private func platformLensType(for device: CaptureDevice) -> PlatformCameraLensType {
|
||||
switch device.deviceType {
|
||||
case .builtInWideAngleCamera:
|
||||
return .wide
|
||||
case .builtInTelephotoCamera:
|
||||
return .telephoto
|
||||
case .builtInUltraWideCamera:
|
||||
return .ultraWide
|
||||
case .builtInDualWideCamera:
|
||||
return .wide
|
||||
default:
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
func create(
|
||||
cameraName: String, settings: PlatformMediaSettings,
|
||||
completion: @escaping (Result<Int64, any Error>) -> Void
|
||||
) {
|
||||
// Create FLTCam only if granted camera access (and audio access if audio is enabled)
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.permissionManager.requestCameraPermission { error in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
// Request audio permission on `create` call with `enableAudio` argument instead of the
|
||||
// `prepareForVideoRecording` call. This is because `prepareForVideoRecording` call is
|
||||
// optional, and used as a workaround to fix a missing frame issue on iOS.
|
||||
if settings.enableAudio {
|
||||
// Setup audio capture session only if granted audio access.
|
||||
strongSelf.permissionManager.requestAudioPermission { [weak self] audioError in
|
||||
// cannot use the outter `strongSelf`
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
if let audioError = audioError {
|
||||
completion(.failure(audioError))
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.createCameraOnSessionQueue(
|
||||
withName: cameraName,
|
||||
settings: settings,
|
||||
completion: completion)
|
||||
}
|
||||
} else {
|
||||
strongSelf.createCameraOnSessionQueue(
|
||||
withName: cameraName,
|
||||
settings: settings,
|
||||
completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createCameraOnSessionQueue(
|
||||
withName: String,
|
||||
settings: PlatformMediaSettings,
|
||||
completion: @escaping (Result<Int64, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.sessionQueueCreateCamera(name: withName, settings: settings, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
// This must be called on captureSessionQueue. It is extracted from createCameraOnSessionQueue
|
||||
// to make it easier to reason about strong/weak self pointers.
|
||||
private func sessionQueueCreateCamera(
|
||||
name: String,
|
||||
settings: PlatformMediaSettings,
|
||||
completion: @escaping (Result<Int64, any Error>) -> Void
|
||||
) {
|
||||
let mediaSettingsAVWrapper = FLTCamMediaSettingsAVWrapper()
|
||||
|
||||
let camConfiguration = CameraConfiguration(
|
||||
mediaSettings: settings,
|
||||
mediaSettingsWrapper: mediaSettingsAVWrapper,
|
||||
captureDeviceFactory: captureDeviceFactory,
|
||||
audioCaptureDeviceFactory: { AVCaptureDevice.default(for: .audio)! },
|
||||
captureSessionFactory: captureSessionFactory,
|
||||
captureSessionQueue: captureSessionQueue,
|
||||
captureDeviceInputFactory: captureDeviceInputFactory,
|
||||
initialCameraName: name
|
||||
)
|
||||
|
||||
do {
|
||||
let newCamera = try DefaultCamera(configuration: camConfiguration)
|
||||
|
||||
camera?.close()
|
||||
camera = newCamera
|
||||
|
||||
ensureToRunOnMainQueue { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
completion(.success(strongSelf.registry.register(newCamera)))
|
||||
}
|
||||
} catch let error as NSError {
|
||||
completion(.failure(CameraPlugin.pigeonErrorFromNSError(error)))
|
||||
}
|
||||
}
|
||||
|
||||
func initialize(
|
||||
cameraId: Int64, imageFormat: PlatformImageFormatGroup,
|
||||
completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.sessionQueueInitializeCamera(
|
||||
cameraId,
|
||||
withImageFormat: imageFormat,
|
||||
completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
// This must be called on captureSessionQueue. It is extracted from initializeCamera to make it
|
||||
// easier to reason about strong/weak self pointers.
|
||||
private func sessionQueueInitializeCamera(
|
||||
_ cameraId: Int64,
|
||||
withImageFormat imageFormat: PlatformImageFormatGroup,
|
||||
completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
guard let camera = camera else { return }
|
||||
|
||||
camera.videoFormat = getPixelFormat(for: imageFormat)
|
||||
|
||||
camera.onFrameAvailable = { [weak self] in
|
||||
guard let camera = self?.camera else { return }
|
||||
if !camera.isPreviewPaused {
|
||||
ensureToRunOnMainQueue {
|
||||
self?.registry.textureFrameAvailable(Int64(cameraId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
camera.dartAPI = CameraEventApi(
|
||||
binaryMessenger: messenger,
|
||||
messageChannelSuffix: "\(cameraId)"
|
||||
)
|
||||
|
||||
camera.reportInitializationState()
|
||||
sendDeviceOrientation(UIDevice.current.orientation)
|
||||
camera.start()
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func startImageStream(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
strongSelf.camera?.startImageStream(with: strongSelf.messenger, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func stopImageStream(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.stopImageStream()
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func receivedImageStreamData(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.receivedImageStreamData()
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func dispose(cameraId: Int64, completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
registry.unregisterTexture(Int64(cameraId))
|
||||
captureSessionQueue.async { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.camera?.close()
|
||||
strongSelf.camera = nil
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func lockCaptureOrientation(
|
||||
orientation: PlatformDeviceOrientation, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.lockCaptureOrientation(orientation)
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func unlockCaptureOrientation(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.unlockCaptureOrientation()
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func takePicture(completion: @escaping (Result<String, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.captureToFile(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func prepareForVideoRecording(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setUpCaptureSessionForAudioIfNeeded()
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func startVideoRecording(
|
||||
enableStream: Bool, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.camera?.startVideoRecording(
|
||||
completion: completion,
|
||||
messengerForStreaming: enableStream ? strongSelf.messenger : nil)
|
||||
}
|
||||
}
|
||||
|
||||
func stopVideoRecording(completion: @escaping (Result<String, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.stopVideoRecording(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func pauseVideoRecording(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.pauseVideoRecording()
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func resumeVideoRecording(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.resumeVideoRecording()
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func setFlashMode(
|
||||
mode: PlatformFlashMode, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setFlashMode(mode, withCompletion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func setExposureMode(
|
||||
mode: PlatformExposureMode, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setExposureMode(mode)
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func setExposurePoint(
|
||||
point: PlatformPoint?, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setExposurePoint(point, withCompletion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func getMinExposureOffset(completion: @escaping (Result<Double, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
if let minOffset = self?.camera?.minimumExposureOffset {
|
||||
completion(.success(minOffset))
|
||||
} else {
|
||||
completion(.success(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getMaxExposureOffset(completion: @escaping (Result<Double, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
if let maxOffset = self?.camera?.maximumExposureOffset {
|
||||
completion(.success(maxOffset))
|
||||
} else {
|
||||
completion(.success(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setExposureOffset(offset: Double, completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setExposureOffset(offset)
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func setFocusMode(
|
||||
mode: PlatformFocusMode, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setFocusMode(mode)
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func setFocusPoint(point: PlatformPoint?, completion: @escaping (Result<Void, any Error>) -> Void)
|
||||
{
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setFocusPoint(point, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func getMinZoomLevel(completion: @escaping (Result<Double, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
if let minZoom = self?.camera?.minimumAvailableZoomFactor {
|
||||
completion(.success(minZoom))
|
||||
} else {
|
||||
completion(.success(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getMaxZoomLevel(completion: @escaping (Result<Double, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
if let maxZoom = self?.camera?.maximumAvailableZoomFactor {
|
||||
completion(.success(maxZoom))
|
||||
} else {
|
||||
completion(.success(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setZoomLevel(zoom: Double, completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setZoomLevel(zoom, withCompletion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func setVideoStabilizationMode(
|
||||
mode: PlatformVideoStabilizationMode, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setVideoStabilizationMode(mode, withCompletion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func isVideoStabilizationModeSupported(
|
||||
mode: PlatformVideoStabilizationMode,
|
||||
completion: @escaping (Result<Bool, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
if let camera = self?.camera {
|
||||
let isSupported = camera.isVideoStabilizationModeSupported(mode)
|
||||
completion(.success(isSupported))
|
||||
} else {
|
||||
completion(.success(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pausePreview(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.pausePreview()
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func resumePreview(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.resumePreview()
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func updateDescriptionWhileRecording(
|
||||
cameraName: String, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setDescriptionWhileRecording(cameraName, withCompletion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func setImageFileFormat(
|
||||
format: PlatformImageFileFormat, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setImageFileFormat(format)
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func setJpegImageQuality(
|
||||
quality: Int64, completion: @escaping (Result<Void, any Error>) -> Void
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.camera?.setJpegImageQuality(quality)
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
|
||||
/// Gets AVCaptureFlashMode from PlatformFlashMode.
|
||||
/// mode - flash mode.
|
||||
func getAVCaptureFlashMode(for mode: PlatformFlashMode) -> AVCaptureDevice.FlashMode {
|
||||
switch mode {
|
||||
case .off:
|
||||
return .off
|
||||
case .auto:
|
||||
return .auto
|
||||
case .always:
|
||||
return .on
|
||||
case .torch:
|
||||
assertionFailure("This mode cannot be converted, and requires custom handling.")
|
||||
return .off
|
||||
@unknown default:
|
||||
assertionFailure("Unknown flash mode")
|
||||
return .off
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets UIDeviceOrientation from its Pigeon representation.
|
||||
/// orientation - the Pigeon device orientation.
|
||||
func getUIDeviceOrientation(
|
||||
for orientation: PlatformDeviceOrientation
|
||||
) -> UIDeviceOrientation {
|
||||
switch orientation {
|
||||
case .portraitDown:
|
||||
return .portraitUpsideDown
|
||||
case .landscapeLeft:
|
||||
return .landscapeLeft
|
||||
case .landscapeRight:
|
||||
return .landscapeRight
|
||||
case .portraitUp:
|
||||
return .portrait
|
||||
@unknown default:
|
||||
assertionFailure("Unknown device orientation")
|
||||
return .portrait
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a Pigeon representation of UIDeviceOrientation.
|
||||
/// orientation - the UIDeviceOrientation.
|
||||
func getPigeonDeviceOrientation(
|
||||
for orientation: UIDeviceOrientation
|
||||
) -> PlatformDeviceOrientation {
|
||||
switch orientation {
|
||||
case .portraitUpsideDown:
|
||||
return .portraitDown
|
||||
case .landscapeLeft:
|
||||
return .landscapeLeft
|
||||
case .landscapeRight:
|
||||
return .landscapeRight
|
||||
case .portrait:
|
||||
return .portraitUp
|
||||
default:
|
||||
return .portraitUp
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets pixel format from its Pigeon representation.
|
||||
/// imageFormat - the Pigeon image format.
|
||||
func getPixelFormat(for imageFormat: PlatformImageFormatGroup) -> OSType {
|
||||
switch imageFormat {
|
||||
case .bgra8888:
|
||||
return kCVPixelFormatType_32BGRA
|
||||
case .yuv420:
|
||||
return kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
|
||||
@unknown default:
|
||||
assertionFailure("Unknown image format")
|
||||
return kCVPixelFormatType_32BGRA
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets video stabilization mode from its Pigeon representation.
|
||||
/// videoStabilizationMode - the Pigeon video stabilization mode.
|
||||
func getAvCaptureVideoStabilizationMode(
|
||||
_ videoStabilizationMode: PlatformVideoStabilizationMode
|
||||
) -> AVCaptureVideoStabilizationMode {
|
||||
|
||||
switch videoStabilizationMode {
|
||||
case .off:
|
||||
return .off
|
||||
case .standard:
|
||||
return .standard
|
||||
case .cinematic:
|
||||
return .cinematic
|
||||
case .cinematicExtended:
|
||||
if #available(iOS 13.0, *) {
|
||||
return .cinematicExtended
|
||||
} else {
|
||||
return .cinematic
|
||||
}
|
||||
@unknown default:
|
||||
return .off
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
/// A protocol which is a direct passthrough to `AVCaptureConnection`. It exists to allow replacing
|
||||
/// `AVCaptureConnection` in tests.
|
||||
protocol CaptureConnection: NSObjectProtocol {
|
||||
/// Corresponds to the `isVideoMirrored` property of `AVCaptureConnection`
|
||||
var isVideoMirrored: Bool { get set }
|
||||
|
||||
/// Corresponds to the `videoOrientation` property of `AVCaptureConnection`
|
||||
var videoOrientation: AVCaptureVideoOrientation { get set }
|
||||
|
||||
/// Corresponds to the `inputPorts` property of `AVCaptureConnection`
|
||||
var inputPorts: [AVCaptureInput.Port] { get }
|
||||
|
||||
/// Corresponds to the `supportsVideoMirroring` property of `AVCaptureConnection`
|
||||
var isVideoMirroringSupported: Bool { get }
|
||||
|
||||
/// Corresponds to the `supportsVideoOrientation` property of `AVCaptureConnection`
|
||||
var isVideoOrientationSupported: Bool { get }
|
||||
|
||||
/// Corresponds to the preferredVideoStabilizationMode property of `AVCaptureConnection`
|
||||
var preferredVideoStabilizationMode: AVCaptureVideoStabilizationMode { get set }
|
||||
|
||||
}
|
||||
|
||||
extension AVCaptureConnection: CaptureConnection {}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
/// A protocol which is a direct passthrough to AVCaptureDevice.
|
||||
/// It exists to allow replacing AVCaptureDevice in tests.
|
||||
protocol CaptureDevice: NSObjectProtocol {
|
||||
/// Underlying `AVCaptureDevice` instance. This is should not be used directly
|
||||
/// in the plugin implementation code, but it exists so that other protocol default
|
||||
/// implementation can pass the raw device to AVFoundation methods.
|
||||
var avDevice: AVCaptureDevice { get }
|
||||
|
||||
// Device identifier
|
||||
var uniqueID: String { get }
|
||||
|
||||
// Position/Orientation
|
||||
var position: AVCaptureDevice.Position { get }
|
||||
|
||||
// Lens type
|
||||
var deviceType: AVCaptureDevice.DeviceType { get }
|
||||
|
||||
// Format/Configuration
|
||||
var flutterActiveFormat: CaptureDeviceFormat { get set }
|
||||
var flutterFormats: [CaptureDeviceFormat] { get }
|
||||
|
||||
// Flash/Torch
|
||||
var hasFlash: Bool { get }
|
||||
var hasTorch: Bool { get }
|
||||
var isTorchAvailable: Bool { get }
|
||||
var torchMode: AVCaptureDevice.TorchMode { get set }
|
||||
func isFlashModeSupported(_ mode: AVCaptureDevice.FlashMode) -> Bool
|
||||
|
||||
// Focus
|
||||
var isFocusPointOfInterestSupported: Bool { get }
|
||||
func isFocusModeSupported(_ mode: AVCaptureDevice.FocusMode) -> Bool
|
||||
var focusMode: AVCaptureDevice.FocusMode { get set }
|
||||
var focusPointOfInterest: CGPoint { get set }
|
||||
|
||||
// Exposure
|
||||
var isExposurePointOfInterestSupported: Bool { get }
|
||||
var exposureMode: AVCaptureDevice.ExposureMode { get set }
|
||||
var exposurePointOfInterest: CGPoint { get set }
|
||||
var minExposureTargetBias: Float { get }
|
||||
var maxExposureTargetBias: Float { get }
|
||||
func setExposureTargetBias(
|
||||
_ bias: Float, completionHandler handler: ((CMTime) -> Void)?)
|
||||
func isExposureModeSupported(_ mode: AVCaptureDevice.ExposureMode) -> Bool
|
||||
|
||||
// Zoom
|
||||
var maxAvailableVideoZoomFactor: CGFloat { get }
|
||||
var minAvailableVideoZoomFactor: CGFloat { get }
|
||||
var videoZoomFactor: CGFloat { get set }
|
||||
|
||||
// Video Stabilization
|
||||
func isVideoStabilizationModeSupported(_ videoStabilizationMode: AVCaptureVideoStabilizationMode)
|
||||
-> Bool
|
||||
|
||||
// Camera Properties
|
||||
var lensAperture: Float { get }
|
||||
var exposureDuration: CMTime { get }
|
||||
var iso: Float { get }
|
||||
|
||||
// Configuration Lock
|
||||
func lockForConfiguration() throws
|
||||
func unlockForConfiguration()
|
||||
|
||||
// Frame Duration
|
||||
var activeVideoMinFrameDuration: CMTime { get set }
|
||||
var activeVideoMaxFrameDuration: CMTime { get set }
|
||||
}
|
||||
|
||||
/// A protocol which is a direct passthrough to AVCaptureInput.
|
||||
/// It exists to allow replacing AVCaptureInput in tests.
|
||||
protocol CaptureInput: NSObjectProtocol {
|
||||
/// Underlying input instance. It is exposed as raw AVCaptureInput has to be passed to some
|
||||
/// AVFoundation methods. The plugin implementation code shouldn't use it though.
|
||||
var avInput: AVCaptureInput { get }
|
||||
|
||||
var ports: [AVCaptureInput.Port] { get }
|
||||
}
|
||||
|
||||
/// A protocol which wraps the creation of AVCaptureDeviceInput.
|
||||
/// It exists to allow mocking instances of AVCaptureDeviceInput in tests.
|
||||
protocol CaptureDeviceInputFactory: NSObjectProtocol {
|
||||
func deviceInput(with device: CaptureDevice) throws -> CaptureInput
|
||||
}
|
||||
|
||||
extension AVCaptureDevice: CaptureDevice {
|
||||
var avDevice: AVCaptureDevice { self }
|
||||
|
||||
var flutterActiveFormat: CaptureDeviceFormat {
|
||||
get { activeFormat }
|
||||
set { activeFormat = newValue.avFormat }
|
||||
}
|
||||
|
||||
var flutterFormats: [CaptureDeviceFormat] { formats }
|
||||
|
||||
func isVideoStabilizationModeSupported(_ videoStabilizationMode: AVCaptureVideoStabilizationMode)
|
||||
-> Bool
|
||||
{
|
||||
return self.activeFormat.isVideoStabilizationModeSupported(videoStabilizationMode)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AVCaptureInput: CaptureInput {
|
||||
var avInput: AVCaptureInput { self }
|
||||
}
|
||||
|
||||
/// A default implementation of CaptureDeviceInputFactory protocol which
|
||||
/// wraps a call to AVCaptureInput static method `deviceInputWithDevice`.
|
||||
class DefaultCaptureDeviceInputFactory: NSObject, CaptureDeviceInputFactory {
|
||||
func deviceInput(with device: CaptureDevice) throws -> CaptureInput {
|
||||
return try AVCaptureDeviceInput(device: device.avDevice)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
/// A protocol which is a direct passthrough to `AVFrameRateRange`. It exists to allow replacing
|
||||
/// `AVFrameRateRange` in tests as it has no public initializer.
|
||||
protocol FrameRateRange: NSObjectProtocol {
|
||||
var minFrameRate: Float64 { get }
|
||||
var maxFrameRate: Float64 { get }
|
||||
}
|
||||
|
||||
/// A protocol which is a direct passthrough to `AVCaptureDeviceFormat`. It exists to allow
|
||||
/// replacing `AVCaptureDeviceFormat` in tests as it has no public initializer.
|
||||
protocol CaptureDeviceFormat: NSObjectProtocol {
|
||||
/// The underlying `AVCaptureDeviceFormat` instance. This exists so that the format
|
||||
/// can be extracted when setting the active format on a device.
|
||||
var avFormat: AVCaptureDevice.Format { get }
|
||||
|
||||
var formatDescription: CMFormatDescription { get }
|
||||
var flutterVideoSupportedFrameRateRanges: [FrameRateRange] { get }
|
||||
}
|
||||
|
||||
extension AVFrameRateRange: FrameRateRange {}
|
||||
|
||||
extension AVCaptureDevice.Format: CaptureDeviceFormat {
|
||||
var avFormat: AVCaptureDevice.Format { self }
|
||||
|
||||
var flutterVideoSupportedFrameRateRanges: [FrameRateRange] { videoSupportedFrameRateRanges }
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
/// A protocol which is a direct passthrough to `AVCaptureOutput`. It exists to allow mocking
|
||||
/// `AVCaptureOutput` in tests.
|
||||
protocol CaptureOutput {
|
||||
/// Returns a connection with the specified media type, or nil if no such connection exists.
|
||||
func connection(with mediaType: AVMediaType) -> CaptureConnection?
|
||||
}
|
||||
|
||||
/// A protocol which is a direct passthrough to `AVCaptureVideoDataOutput`. It exists to allow
|
||||
/// mocking `AVCaptureVideoDataOutput` in tests.
|
||||
protocol CaptureVideoDataOutput: CaptureOutput {
|
||||
/// The underlying instance of `AVCaptureVideoDataOutput`.
|
||||
var avOutput: AVCaptureVideoDataOutput { get }
|
||||
|
||||
/// Corresponds to the `alwaysDiscardsLateVideoFrames` property of `AVCaptureVideoDataOutput`
|
||||
var alwaysDiscardsLateVideoFrames: Bool { get set }
|
||||
|
||||
/// Corresponds to the `availableVideoPixelFormatTypes` property of `AVCaptureVideoDataOutput`
|
||||
var availableVideoPixelFormatTypes: [FourCharCode] { get }
|
||||
|
||||
/// Corresponds to the `videoSettings` property of `AVCaptureVideoDataOutput`
|
||||
var videoSettings: [String: Any]! { get set }
|
||||
|
||||
/// Corresponds to the `setSampleBufferDelegate` method of `AVCaptureVideoDataOutput`
|
||||
func setSampleBufferDelegate(
|
||||
_ sampleBufferDelegate: AVCaptureVideoDataOutputSampleBufferDelegate?,
|
||||
queue sampleBufferCallbackQueue: DispatchQueue?
|
||||
)
|
||||
}
|
||||
|
||||
extension AVCaptureVideoDataOutput: CaptureVideoDataOutput {
|
||||
var avOutput: AVCaptureVideoDataOutput {
|
||||
return self
|
||||
}
|
||||
|
||||
func connection(with mediaType: AVMediaType) -> CaptureConnection? {
|
||||
let connection: AVCaptureConnection? = connection(with: mediaType)
|
||||
return connection
|
||||
}
|
||||
}
|
||||
|
||||
/// A protocol which is a direct passthrough to `AVCapturePhotoOutput`. It exists to allow mocking
|
||||
/// `AVCapturePhotoOutput` in tests.
|
||||
protocol CapturePhotoOutput: CaptureOutput {
|
||||
/// The underlying instance of `AVCapturePhotoOutput`.
|
||||
var avOutput: AVCapturePhotoOutput { get }
|
||||
|
||||
/// Corresponds to the `availablePhotoCodecTypes` property of `AVCapturePhotoOutput`
|
||||
var availablePhotoCodecTypes: [AVVideoCodecType] { get }
|
||||
|
||||
/// Corresponds to the `isHighResolutionCaptureEnabled` property of `AVCapturePhotoOutput`
|
||||
var isHighResolutionCaptureEnabled: Bool { get set }
|
||||
|
||||
/// Corresponds to the `supportedFlashModes` property of `AVCapturePhotoOutput`
|
||||
var supportedFlashModes: [AVCaptureDevice.FlashMode] { get }
|
||||
|
||||
/// Corresponds to the `capturePhotoWithSettings` method of `AVCapturePhotoOutput`
|
||||
func capturePhoto(with settings: AVCapturePhotoSettings, delegate: AVCapturePhotoCaptureDelegate)
|
||||
}
|
||||
|
||||
/// Make AVCapturePhotoOutput conform to FLTCapturePhotoOutput protocol directly
|
||||
extension AVCapturePhotoOutput: CapturePhotoOutput {
|
||||
var avOutput: AVCapturePhotoOutput {
|
||||
return self
|
||||
}
|
||||
|
||||
func connection(with mediaType: AVMediaType) -> CaptureConnection? {
|
||||
// Explicit type is required to access the underlying AVCapturePhotoOutput.connection method
|
||||
let connection: AVCaptureConnection? = connection(with: mediaType)
|
||||
return connection
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
/// A protocol which is a direct passthrough to AVCaptureSession.
|
||||
/// It exists to allow replacing AVCaptureSession in tests.
|
||||
protocol CaptureSession: NSObjectProtocol {
|
||||
var sessionPreset: AVCaptureSession.Preset { get set }
|
||||
var inputs: [AVCaptureInput] { get }
|
||||
var outputs: [AVCaptureOutput] { get }
|
||||
var automaticallyConfiguresApplicationAudioSession: Bool { get set }
|
||||
var isRunning: Bool { get }
|
||||
|
||||
func beginConfiguration()
|
||||
func commitConfiguration()
|
||||
func startRunning()
|
||||
func stopRunning()
|
||||
func canSetSessionPreset(_ preset: AVCaptureSession.Preset) -> Bool
|
||||
func addInputWithNoConnections(_ input: CaptureInput)
|
||||
func addOutputWithNoConnections(_ output: AVCaptureOutput)
|
||||
func addConnection(_ connection: AVCaptureConnection)
|
||||
func addInput(_ input: CaptureInput)
|
||||
func addOutput(_ output: AVCaptureOutput)
|
||||
func removeInput(_ input: CaptureInput)
|
||||
func removeOutput(_ output: AVCaptureOutput)
|
||||
func canAddInput(_ input: CaptureInput) -> Bool
|
||||
func canAddOutput(_ output: AVCaptureOutput) -> Bool
|
||||
func canAddConnection(_ connection: AVCaptureConnection) -> Bool
|
||||
}
|
||||
|
||||
extension AVCaptureSession: CaptureSession {
|
||||
func addInputWithNoConnections(_ input: CaptureInput) {
|
||||
addInputWithNoConnections(input.avInput)
|
||||
}
|
||||
|
||||
func addInput(_ input: CaptureInput) {
|
||||
addInput(input.avInput)
|
||||
}
|
||||
|
||||
func removeInput(_ input: CaptureInput) {
|
||||
removeInput(input.avInput)
|
||||
}
|
||||
|
||||
func canAddInput(_ input: CaptureInput) -> Bool {
|
||||
canAddInput(input.avInput)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import UIKit
|
||||
|
||||
/// A protocol which provides the current device orientation.
|
||||
/// It exists to allow replacing UIDevice in tests.
|
||||
protocol DeviceOrientationProvider {
|
||||
/// Returns the physical orientation of the device.
|
||||
var orientation: UIDeviceOrientation { get }
|
||||
}
|
||||
|
||||
/// A default implementation of DeviceOrientationProvider which uses orientation
|
||||
/// of the current device from UIDevice.
|
||||
@objc public class DefaultDeviceOrientationProvider: NSObject, DeviceOrientationProvider {
|
||||
@objc public var orientation: UIDeviceOrientation {
|
||||
return UIDevice.current.orientation
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
/// Determines the video dimensions (width and height) for a given capture device format.
|
||||
/// Used in tests to mock CMVideoFormatDescriptionGetDimensions.
|
||||
typealias VideoDimensionsConverter = (CaptureDeviceFormat) -> CMVideoDimensions
|
||||
|
||||
enum FormatUtils {
|
||||
/// Returns frame rate supported by format closest to targetFrameRate.
|
||||
static private func bestFrameRate(for format: CaptureDeviceFormat, targetFrameRate: Double)
|
||||
-> Double
|
||||
{
|
||||
var bestFrameRate: Double = 0
|
||||
var minDistance: Double = Double.greatestFiniteMagnitude
|
||||
|
||||
for range in format.flutterVideoSupportedFrameRateRanges {
|
||||
let frameRate = min(
|
||||
max(targetFrameRate, Double(range.minFrameRate)), Double(range.maxFrameRate))
|
||||
let distance = abs(frameRate - targetFrameRate)
|
||||
if distance < minDistance {
|
||||
bestFrameRate = frameRate
|
||||
minDistance = distance
|
||||
}
|
||||
}
|
||||
|
||||
return bestFrameRate
|
||||
}
|
||||
|
||||
/// Finds format with same resolution as current activeFormat in captureDevice for which
|
||||
/// bestFrameRate returned frame rate closest to mediaSettings.framesPerSecond.
|
||||
/// Preferred are formats with the same subtype as current activeFormat. Returns this format
|
||||
/// and frame rate which bestFrameRate returned for that format.
|
||||
static func findBestFormat(
|
||||
for captureDevice: CaptureDevice,
|
||||
mediaSettings: PlatformMediaSettings,
|
||||
videoDimensionsConverter: VideoDimensionsConverter
|
||||
) -> (format: CaptureDeviceFormat, frameRate: Double) {
|
||||
let targetResolution = videoDimensionsConverter(captureDevice.flutterActiveFormat)
|
||||
let targetFrameRate = Double(mediaSettings.framesPerSecond ?? 0)
|
||||
let preferredSubType = CMFormatDescriptionGetMediaSubType(
|
||||
captureDevice.flutterActiveFormat.formatDescription)
|
||||
|
||||
var bestFormat = captureDevice.flutterActiveFormat
|
||||
var resolvedBestFrameRate = bestFrameRate(for: bestFormat, targetFrameRate: targetFrameRate)
|
||||
var minDistance = abs(resolvedBestFrameRate - targetFrameRate)
|
||||
var isBestSubTypePreferred = true
|
||||
|
||||
for format in captureDevice.flutterFormats {
|
||||
let resolution = videoDimensionsConverter(format)
|
||||
if resolution.width != targetResolution.width || resolution.height != targetResolution.height
|
||||
{
|
||||
continue
|
||||
}
|
||||
|
||||
let frameRate = bestFrameRate(for: format, targetFrameRate: targetFrameRate)
|
||||
let distance = abs(frameRate - targetFrameRate)
|
||||
let subType = CMFormatDescriptionGetMediaSubType(format.formatDescription)
|
||||
let isSubTypePreferred = subType == preferredSubType
|
||||
|
||||
if distance < minDistance
|
||||
|| (distance == minDistance && isSubTypePreferred && !isBestSubTypePreferred)
|
||||
{
|
||||
bestFormat = format
|
||||
resolvedBestFrameRate = frameRate
|
||||
minDistance = distance
|
||||
isBestSubTypePreferred = isSubTypePreferred
|
||||
}
|
||||
}
|
||||
|
||||
return (bestFormat, resolvedBestFrameRate)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import Flutter
|
||||
|
||||
/// Handles streaming of camera image data to Dart via Flutter event channels.
|
||||
protocol ImageStreamHandler: ImageDataStreamStreamHandler {
|
||||
/// The queue on which `eventSink` property should be accessed.
|
||||
var captureSessionQueue: DispatchQueue { get }
|
||||
|
||||
/// The event sink to stream camera events to Dart.
|
||||
///
|
||||
/// The property should only be accessed on `captureSessionQueue`.
|
||||
/// The block itself should be invoked on the main queue.
|
||||
var eventSink: PigeonEventSink<PlatformCameraImageData>? { get set }
|
||||
}
|
||||
|
||||
/// Default implementation of ImageStreamHandler.
|
||||
class DefaultImageStreamHandler: ImageDataStreamStreamHandler, ImageStreamHandler {
|
||||
let captureSessionQueue: DispatchQueue
|
||||
var eventSink: PigeonEventSink<PlatformCameraImageData>?
|
||||
|
||||
/// Initialize an image stream handler.
|
||||
/// captureSessionQueue - the queue on which the event sink should be accessed.
|
||||
init(captureSessionQueue: DispatchQueue) {
|
||||
self.captureSessionQueue = captureSessionQueue
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func onListen(
|
||||
withArguments arguments: Any?, sink: PigeonEventSink<PlatformCameraImageData>
|
||||
) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.eventSink = sink
|
||||
}
|
||||
}
|
||||
|
||||
override func onCancel(withArguments arguments: Any?) {
|
||||
captureSessionQueue.async { [weak self] in
|
||||
self?.eventSink = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import CoreMedia
|
||||
|
||||
/// An interface for performing media settings operations.
|
||||
///
|
||||
/// xctest-expectation-checking implementation (`TestMediaSettingsAVWrapper`) of this interface can
|
||||
/// be injected into `camera-avfoundation` plugin allowing to run media-settings tests without any
|
||||
/// additional mocking of AVFoundation classes.
|
||||
class FLTCamMediaSettingsAVWrapper {
|
||||
|
||||
/// Requests exclusive access to configure device hardware properties.
|
||||
/// - Parameter captureDevice: The capture device.
|
||||
/// - Throws: An error if the device could not be locked for configuration.
|
||||
/// - Returns: A Bool indicating whether the device was successfully locked for configuration.
|
||||
func lockDevice(_ captureDevice: CaptureDevice) throws {
|
||||
return try captureDevice.lockForConfiguration()
|
||||
}
|
||||
|
||||
/// Release exclusive control over device hardware properties.
|
||||
/// - Parameter captureDevice: The capture device.
|
||||
func unlockDevice(_ captureDevice: CaptureDevice) {
|
||||
captureDevice.unlockForConfiguration()
|
||||
}
|
||||
|
||||
/// When paired with commitConfiguration, allows a client to batch multiple configuration
|
||||
/// operations on a running session into atomic updates.
|
||||
/// - Parameter videoCaptureSession: The video capture session.
|
||||
func beginConfiguration(for videoCaptureSession: CaptureSession) {
|
||||
videoCaptureSession.beginConfiguration()
|
||||
}
|
||||
|
||||
/// When preceded by beginConfiguration, allows a client to batch multiple configuration
|
||||
/// operations on a running session into atomic updates.
|
||||
/// - Parameter videoCaptureSession: The video capture session.
|
||||
func commitConfiguration(for videoCaptureSession: CaptureSession) {
|
||||
videoCaptureSession.commitConfiguration()
|
||||
}
|
||||
|
||||
/// Set receiver's current active minimum frame duration (the reciprocal of its max frame rate).
|
||||
/// - Parameters:
|
||||
/// - duration: The frame duration.
|
||||
/// - captureDevice: The capture device
|
||||
func setMinFrameDuration(_ duration: CMTime, on captureDevice: CaptureDevice) {
|
||||
captureDevice.activeVideoMinFrameDuration = duration
|
||||
}
|
||||
|
||||
/// Set receiver's current active maximum frame duration (the reciprocal of its min frame rate).
|
||||
/// - Parameters:
|
||||
/// - duration: The frame duration.
|
||||
/// - captureDevice: The capture device
|
||||
func setMaxFrameDuration(_ duration: CMTime, on captureDevice: CaptureDevice) {
|
||||
captureDevice.activeVideoMaxFrameDuration = duration
|
||||
}
|
||||
|
||||
/// Creates a new input of the audio media type to receive sample buffers for writing to
|
||||
/// the output file.
|
||||
/// - Parameter outputSettings: The settings used for encoding the audio appended to the output.
|
||||
/// - Returns: An instance of `AssetWriterInput`.
|
||||
func assetWriterAudioInput(withOutputSettings outputSettings: [String: Any]?)
|
||||
-> AssetWriterInput
|
||||
{
|
||||
return AVAssetWriterInput(mediaType: .audio, outputSettings: outputSettings)
|
||||
}
|
||||
|
||||
/// Creates a new input of the video media type to receive sample buffers for writing to
|
||||
/// the output file.
|
||||
/// - Parameter outputSettings: The settings used for encoding the video appended to the output.
|
||||
/// - Returns: An instance of `AssetWriterInput`.
|
||||
func assetWriterVideoInput(withOutputSettings outputSettings: [String: Any]?)
|
||||
-> AssetWriterInput
|
||||
{
|
||||
return AVAssetWriterInput(mediaType: .video, outputSettings: outputSettings)
|
||||
}
|
||||
|
||||
/// Adds an input to the asset writer.
|
||||
/// - Parameters:
|
||||
/// - writerInput: The `AssetWriterInput` object to be added.
|
||||
/// - writer: The `AssetWriter` object.
|
||||
func addInput(_ writerInput: AssetWriterInput, to writer: AssetWriter) {
|
||||
writer.add(writerInput.avInput)
|
||||
}
|
||||
|
||||
/// Specifies the recommended video settings for `FLTCaptureVideoDataOutput`.
|
||||
/// - Parameters:
|
||||
/// - fileType: Specifies the UTI of the file type to be written (see AVMediaFormat.h for a list
|
||||
/// of file format UTIs).
|
||||
/// - output: The `FLTCaptureVideoDataOutput` instance.
|
||||
/// - Returns: A fully populated dictionary of keys and values that are compatible with AVAssetWriter.
|
||||
func recommendedVideoSettingsForAssetWriter(
|
||||
withFileType fileType: AVFileType,
|
||||
for output: CaptureVideoDataOutput
|
||||
) -> [String: Any]? {
|
||||
return output.avOutput.recommendedVideoSettingsForAssetWriter(writingTo: fileType)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
|
||||
/// A protocol for permission-related operations on AVCaptureDevice.
|
||||
/// It exists to allow mocking permission checks in tests.
|
||||
protocol PermissionServicing: NSObjectProtocol {
|
||||
func authorizationStatus(for mediaType: AVMediaType) -> AVAuthorizationStatus
|
||||
func requestAccess(
|
||||
for mediaType: AVMediaType,
|
||||
completion handler: @escaping @Sendable (Bool) -> Void
|
||||
)
|
||||
}
|
||||
|
||||
/// Default implementation of PermissionServicing that forwards calls to AVCaptureDevice.
|
||||
class DefaultPermissionService: NSObject, PermissionServicing {
|
||||
func authorizationStatus(for mediaType: AVMediaType) -> AVAuthorizationStatus {
|
||||
return AVCaptureDevice.authorizationStatus(for: mediaType)
|
||||
}
|
||||
|
||||
func requestAccess(
|
||||
for mediaType: AVMediaType,
|
||||
completion handler: @escaping @Sendable (Bool) -> Void
|
||||
) {
|
||||
AVCaptureDevice.requestAccess(for: mediaType, completionHandler: handler)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import Dispatch
|
||||
import Foundation
|
||||
|
||||
/// Queue-specific context data to be associated with the capture session queue.
|
||||
let captureSessionQueueSpecificKey = DispatchSpecificKey<String>()
|
||||
let captureSessionQueueSpecificValue = "capture_session_queue"
|
||||
|
||||
/// Ensures the given block to be run on the main queue.
|
||||
/// If caller site is already on the main queue, the block will be run
|
||||
/// synchronously. Otherwise, the block will be dispatched asynchronously to the
|
||||
/// main queue.
|
||||
/// block - the block to be run on the main queue.
|
||||
func ensureToRunOnMainQueue(_ block: @escaping () -> Void) {
|
||||
if Thread.isMainThread {
|
||||
block()
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
block()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import AVFoundation
|
||||
import Flutter
|
||||
import Foundation
|
||||
|
||||
/// The completion handler block for save photo operations.
|
||||
/// Can be called from either main queue or IO queue.
|
||||
/// If success, `path` will be present and `error` will be nil. Otherwise, `path` will be nil and
|
||||
/// `error` will be present.
|
||||
/// path - the path for successfully saved photo file.
|
||||
/// error - photo capture error or IO error.
|
||||
typealias SavePhotoDelegateCompletionHandler = (String?, Error?) -> Void
|
||||
|
||||
/// Delegate object that handles photo capture results.
|
||||
class SavePhotoDelegate: NSObject, AVCapturePhotoCaptureDelegate {
|
||||
/// The file path for the captured photo.
|
||||
private let path: String
|
||||
|
||||
/// The queue on which captured photos are written to disk.
|
||||
private let ioQueue: DispatchQueue
|
||||
|
||||
/// The completion handler block for capture and save photo operations.
|
||||
let completionHandler: SavePhotoDelegateCompletionHandler
|
||||
|
||||
/// The path for captured photo file.
|
||||
/// Exposed for unit tests to verify the captured photo file path.
|
||||
var filePath: String {
|
||||
path
|
||||
}
|
||||
|
||||
/// Initialize a photo capture delegate.
|
||||
/// path - the path for captured photo file.
|
||||
/// ioQueue - the queue on which captured photos are written to disk.
|
||||
/// completionHandler - The completion handler block for save photo operations. Can
|
||||
/// be called from either main queue or IO queue.
|
||||
init(
|
||||
path: String,
|
||||
ioQueue: DispatchQueue,
|
||||
completionHandler: @escaping SavePhotoDelegateCompletionHandler
|
||||
) {
|
||||
self.path = path
|
||||
self.ioQueue = ioQueue
|
||||
self.completionHandler = completionHandler
|
||||
super.init()
|
||||
}
|
||||
|
||||
/// Handler to write captured photo data into a file.
|
||||
/// - Parameters:
|
||||
/// - error: The capture error
|
||||
/// - photoDataProvider: A closure that provides photo data
|
||||
func handlePhotoCaptureResult(
|
||||
error: Error?,
|
||||
photoDataProvider: @escaping () -> WritableData?
|
||||
) {
|
||||
if let error = error {
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
ioQueue.async { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
do {
|
||||
let data = photoDataProvider()
|
||||
try data?.writeToPath(strongSelf.path, options: .atomic)
|
||||
strongSelf.completionHandler(strongSelf.path, nil)
|
||||
} catch {
|
||||
strongSelf.completionHandler(nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func photoOutput(
|
||||
_ output: AVCapturePhotoOutput,
|
||||
didFinishProcessingPhoto photo: AVCapturePhoto,
|
||||
error: Error?
|
||||
) {
|
||||
handlePhotoCaptureResult(error: error) {
|
||||
photo.fileDataRepresentation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A protocol which abstracts the file writing operation implemented in `Data`.
|
||||
/// It exists to allow replacing `Data` in tests.
|
||||
protocol WritableData {
|
||||
func writeToPath(
|
||||
_ path: String,
|
||||
options: Data.WritingOptions
|
||||
) throws
|
||||
}
|
||||
|
||||
extension Data: WritableData {
|
||||
func writeToPath(_ path: String, options: Data.WritingOptions) throws {
|
||||
try write(to: URL(fileURLWithPath: path), options: options)
|
||||
}
|
||||
}
|
||||
5
camera_avfoundation/lib/camera_avfoundation.dart
Normal file
5
camera_avfoundation/lib/camera_avfoundation.dart
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
export 'src/avfoundation_camera.dart';
|
||||
650
camera_avfoundation/lib/src/avfoundation_camera.dart
Normal file
650
camera_avfoundation/lib/src/avfoundation_camera.dart
Normal file
|
|
@ -0,0 +1,650 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:camera_platform_interface/camera_platform_interface.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:stream_transform/stream_transform.dart';
|
||||
|
||||
import 'messages.g.dart';
|
||||
import 'type_conversion.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
/// An iOS implementation of [CameraPlatform] based on AVFoundation.
|
||||
class AVFoundationCamera extends CameraPlatform {
|
||||
/// Creates a new AVFoundation-based [CameraPlatform] implementation instance.
|
||||
AVFoundationCamera({@visibleForTesting CameraApi? api}) : _hostApi = api ?? CameraApi();
|
||||
|
||||
/// Registers this class as the default instance of [CameraPlatform].
|
||||
static void registerWith() {
|
||||
CameraPlatform.instance = AVFoundationCamera();
|
||||
}
|
||||
|
||||
/// Interface for calling host-side code.
|
||||
final CameraApi _hostApi;
|
||||
|
||||
/// The controller we need to broadcast the different events coming
|
||||
/// from handleMethodCall, specific to camera events.
|
||||
///
|
||||
/// It is a `broadcast` because multiple controllers will connect to
|
||||
/// different stream views of this Controller.
|
||||
/// This is only exposed for test purposes. It shouldn't be used by clients of
|
||||
/// the plugin as it may break or change at any time.
|
||||
@visibleForTesting
|
||||
final StreamController<CameraEvent> cameraEventStreamController =
|
||||
StreamController<CameraEvent>.broadcast();
|
||||
|
||||
/// The handler for device-level messages that should be rebroadcast to
|
||||
/// clients as [DeviceEvent]s.
|
||||
@visibleForTesting
|
||||
late final HostDeviceMessageHandler hostHandler = () {
|
||||
// Set up the method handler lazily.
|
||||
return HostDeviceMessageHandler();
|
||||
}();
|
||||
|
||||
/// The per-camera handlers for messages that should be rebroadcast to
|
||||
/// clients as [CameraEvent]s.
|
||||
@visibleForTesting
|
||||
final Map<int, HostCameraMessageHandler> hostCameraHandlers = <int, HostCameraMessageHandler>{};
|
||||
|
||||
// The stream to receive frames from the native code.
|
||||
StreamSubscription<dynamic>? _platformImageStreamSubscription;
|
||||
|
||||
// The stream for vending frames to platform interface clients.
|
||||
StreamController<CameraImageData>? _frameStreamController;
|
||||
|
||||
Stream<CameraEvent> _cameraEvents(int cameraId) =>
|
||||
cameraEventStreamController.stream.where((CameraEvent event) => event.cameraId == cameraId);
|
||||
|
||||
@override
|
||||
Future<List<CameraDescription>> availableCameras() async {
|
||||
try {
|
||||
return (await _hostApi.getAvailableCameras()).map(cameraDescriptionFromPlatform).toList();
|
||||
} on PlatformException catch (e) {
|
||||
throw CameraException(e.code, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> createCamera(
|
||||
CameraDescription cameraDescription,
|
||||
ResolutionPreset? resolutionPreset, {
|
||||
bool enableAudio = false,
|
||||
}) => createCameraWithSettings(
|
||||
cameraDescription,
|
||||
MediaSettings(resolutionPreset: resolutionPreset, enableAudio: enableAudio),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<int> createCameraWithSettings(
|
||||
CameraDescription cameraDescription,
|
||||
MediaSettings? mediaSettings,
|
||||
) async {
|
||||
try {
|
||||
return await _hostApi.create(
|
||||
cameraDescription.name,
|
||||
PlatformMediaSettings(
|
||||
resolutionPreset: _pigeonResolutionPreset(mediaSettings?.resolutionPreset),
|
||||
framesPerSecond: mediaSettings?.fps,
|
||||
videoBitrate: mediaSettings?.videoBitrate,
|
||||
audioBitrate: mediaSettings?.audioBitrate,
|
||||
enableAudio: mediaSettings?.enableAudio ?? true,
|
||||
),
|
||||
);
|
||||
} on PlatformException catch (e) {
|
||||
throw CameraException(e.code, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> initializeCamera(
|
||||
int cameraId, {
|
||||
ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown,
|
||||
}) async {
|
||||
hostCameraHandlers.putIfAbsent(
|
||||
cameraId,
|
||||
() => HostCameraMessageHandler(cameraId, cameraEventStreamController),
|
||||
);
|
||||
|
||||
final completer = Completer<void>();
|
||||
|
||||
unawaited(
|
||||
onCameraInitialized(cameraId).first.then((CameraInitializedEvent value) {
|
||||
completer.complete();
|
||||
}),
|
||||
);
|
||||
|
||||
try {
|
||||
await _hostApi.initialize(cameraId, _pigeonImageFormat(imageFormatGroup));
|
||||
} on PlatformException catch (e, s) {
|
||||
completer.completeError(CameraException(e.code, e.message), s);
|
||||
}
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose(int cameraId) async {
|
||||
final HostCameraMessageHandler? handler = hostCameraHandlers.remove(cameraId);
|
||||
handler?.dispose();
|
||||
|
||||
await _hostApi.dispose(cameraId);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<CameraInitializedEvent> onCameraInitialized(int cameraId) {
|
||||
return _cameraEvents(cameraId).whereType<CameraInitializedEvent>();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<CameraResolutionChangedEvent> onCameraResolutionChanged(int cameraId) {
|
||||
return _cameraEvents(cameraId).whereType<CameraResolutionChangedEvent>();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<CameraClosingEvent> onCameraClosing(int cameraId) {
|
||||
return _cameraEvents(cameraId).whereType<CameraClosingEvent>();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<CameraErrorEvent> onCameraError(int cameraId) {
|
||||
return _cameraEvents(cameraId).whereType<CameraErrorEvent>();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<VideoRecordedEvent> onVideoRecordedEvent(int cameraId) {
|
||||
return _cameraEvents(cameraId).whereType<VideoRecordedEvent>();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<DeviceOrientationChangedEvent> onDeviceOrientationChanged() {
|
||||
return hostHandler.deviceEventStreamController.stream
|
||||
.whereType<DeviceOrientationChangedEvent>();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> lockCaptureOrientation(int cameraId, DeviceOrientation orientation) async {
|
||||
await _hostApi.lockCaptureOrientation(serializeDeviceOrientation(orientation));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> unlockCaptureOrientation(int cameraId) async {
|
||||
await _hostApi.unlockCaptureOrientation();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<XFile> takePicture(int cameraId) async {
|
||||
final String path = await _hostApi.takePicture();
|
||||
return XFile(path);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> prepareForVideoRecording() async {
|
||||
await _hostApi.prepareForVideoRecording();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startVideoRecording(int cameraId, {Duration? maxVideoDuration}) async {
|
||||
// Ignore maxVideoDuration, as it is unimplemented and deprecated.
|
||||
return startVideoCapturing(VideoCaptureOptions(cameraId));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startVideoCapturing(VideoCaptureOptions options) async {
|
||||
// Max video duration is currently not supported.
|
||||
await _hostApi.startVideoRecording(options.streamCallback != null);
|
||||
|
||||
if (options.streamCallback != null) {
|
||||
_frameStreamController = _createStreamController();
|
||||
_frameStreamController!.stream.listen(options.streamCallback);
|
||||
_startStreamListener();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<XFile> stopVideoRecording(int cameraId) async {
|
||||
final String path = await _hostApi.stopVideoRecording();
|
||||
return XFile(path);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pauseVideoRecording(int cameraId) async {
|
||||
await _hostApi.pauseVideoRecording();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> resumeVideoRecording(int cameraId) async {
|
||||
await _hostApi.resumeVideoRecording();
|
||||
}
|
||||
|
||||
@override
|
||||
bool supportsImageStreaming() => true;
|
||||
|
||||
@override
|
||||
Stream<CameraImageData> onStreamedFrameAvailable(
|
||||
int cameraId, {
|
||||
CameraImageStreamOptions? options,
|
||||
}) {
|
||||
_frameStreamController = _createStreamController(onListen: _onFrameStreamListen);
|
||||
return _frameStreamController!.stream;
|
||||
}
|
||||
|
||||
StreamController<CameraImageData> _createStreamController({void Function()? onListen}) {
|
||||
return StreamController<CameraImageData>(
|
||||
onListen: onListen ?? () {},
|
||||
onPause: _onFrameStreamPauseResume,
|
||||
onResume: _onFrameStreamPauseResume,
|
||||
onCancel: _onFrameStreamCancel,
|
||||
);
|
||||
}
|
||||
|
||||
void _onFrameStreamListen() {
|
||||
_startPlatformStream();
|
||||
}
|
||||
|
||||
Future<void> _startPlatformStream() async {
|
||||
await _hostApi.startImageStream();
|
||||
_startStreamListener();
|
||||
}
|
||||
|
||||
void _startStreamListener() {
|
||||
_platformImageStreamSubscription = imageDataStream().listen((
|
||||
PlatformCameraImageData imageData,
|
||||
) {
|
||||
try {
|
||||
_hostApi.receivedImageStreamData();
|
||||
} on PlatformException catch (e) {
|
||||
throw CameraException(e.code, e.message);
|
||||
}
|
||||
_frameStreamController!.add(cameraImageFromPlatformData(imageData));
|
||||
});
|
||||
}
|
||||
|
||||
FutureOr<void> _onFrameStreamCancel() async {
|
||||
await _hostApi.stopImageStream();
|
||||
await _platformImageStreamSubscription?.cancel();
|
||||
_platformImageStreamSubscription = null;
|
||||
_frameStreamController = null;
|
||||
}
|
||||
|
||||
void _onFrameStreamPauseResume() {
|
||||
throw CameraException(
|
||||
'InvalidCall',
|
||||
'Pause and resume are not supported for onStreamedFrameAvailable',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setFlashMode(int cameraId, FlashMode mode) async {
|
||||
await _hostApi.setFlashMode(_pigeonFlashMode(mode));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setExposureMode(int cameraId, ExposureMode mode) async {
|
||||
await _hostApi.setExposureMode(_pigeonExposureMode(mode));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setExposurePoint(int cameraId, Point<double>? point) async {
|
||||
assert(point == null || point.x >= 0 && point.x <= 1);
|
||||
assert(point == null || point.y >= 0 && point.y <= 1);
|
||||
|
||||
await _hostApi.setExposurePoint(_pigeonPoint(point));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> getMinExposureOffset(int cameraId) async {
|
||||
return _hostApi.getMinExposureOffset();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> getMaxExposureOffset(int cameraId) async {
|
||||
return _hostApi.getMaxExposureOffset();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> getExposureOffsetStepSize(int cameraId) async {
|
||||
// iOS has no step size.
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> setExposureOffset(int cameraId, double offset) async {
|
||||
await _hostApi.setExposureOffset(offset);
|
||||
// The platform API allows for implementations that have to adjust the
|
||||
// target offset and return the actual offset used, but there is never
|
||||
// adjustment in this implementation.
|
||||
return offset;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setFocusMode(int cameraId, FocusMode mode) async {
|
||||
await _hostApi.setFocusMode(_pigeonFocusMode(mode));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setFocusPoint(int cameraId, Point<double>? point) async {
|
||||
assert(point == null || point.x >= 0 && point.x <= 1);
|
||||
assert(point == null || point.y >= 0 && point.y <= 1);
|
||||
|
||||
await _hostApi.setFocusPoint(_pigeonPoint(point));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> getMaxZoomLevel(int cameraId) async {
|
||||
return _hostApi.getMaxZoomLevel();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> getMinZoomLevel(int cameraId) async {
|
||||
return _hostApi.getMinZoomLevel();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setZoomLevel(int cameraId, double zoom) async {
|
||||
try {
|
||||
await _hostApi.setZoomLevel(zoom);
|
||||
} on PlatformException catch (e) {
|
||||
throw CameraException(e.code, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setVideoStabilizationMode(int cameraId, VideoStabilizationMode mode) async {
|
||||
try {
|
||||
final Map<VideoStabilizationMode, PlatformVideoStabilizationMode> availableModes =
|
||||
await _getSupportedVideoStabilizationModeMap(cameraId);
|
||||
|
||||
final PlatformVideoStabilizationMode? platformMode = availableModes[mode];
|
||||
if (platformMode == null) {
|
||||
throw ArgumentError('Unavailable video stabilization mode.', 'mode');
|
||||
}
|
||||
await _hostApi.setVideoStabilizationMode(platformMode);
|
||||
} on PlatformException catch (e) {
|
||||
throw CameraException(e.code, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Iterable<VideoStabilizationMode>> getSupportedVideoStabilizationModes(int cameraId) async {
|
||||
return (await _getSupportedVideoStabilizationModeMap(cameraId)).keys;
|
||||
}
|
||||
|
||||
Future<Map<VideoStabilizationMode, PlatformVideoStabilizationMode>>
|
||||
_getSupportedVideoStabilizationModeMap(int cameraId) async {
|
||||
final ret = <VideoStabilizationMode, PlatformVideoStabilizationMode>{};
|
||||
|
||||
for (final VideoStabilizationMode mode in VideoStabilizationMode.values) {
|
||||
final PlatformVideoStabilizationMode? platformMode = _pigeonVideoStabilizationMode(mode);
|
||||
if (platformMode != null) {
|
||||
final bool isSupported = await _hostApi.isVideoStabilizationModeSupported(platformMode);
|
||||
if (isSupported) {
|
||||
ret[mode] = platformMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pausePreview(int cameraId) async {
|
||||
await _hostApi.pausePreview();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> resumePreview(int cameraId) async {
|
||||
await _hostApi.resumePreview();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setDescriptionWhileRecording(CameraDescription description) async {
|
||||
await _hostApi.updateDescriptionWhileRecording(description.name);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setImageFileFormat(int cameraId, ImageFileFormat format) async {
|
||||
await _hostApi.setImageFileFormat(_pigeonImageFileFormat(format));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setJpegImageQuality(int cameraId, int quality) async {
|
||||
await _hostApi.setJpegImageQuality(quality);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPreview(int cameraId) {
|
||||
return Texture(textureId: cameraId);
|
||||
}
|
||||
|
||||
/// Returns an [FocusMode]'s Pigeon representation.
|
||||
PlatformFocusMode _pigeonFocusMode(FocusMode mode) {
|
||||
switch (mode) {
|
||||
case FocusMode.locked:
|
||||
return PlatformFocusMode.locked;
|
||||
case FocusMode.auto:
|
||||
return PlatformFocusMode.auto;
|
||||
}
|
||||
// The enum comes from a different package, which could get a new value at
|
||||
// any time, so provide a fallback that ensures this won't break when used
|
||||
// with a version that contains new values. This is deliberately outside
|
||||
// the switch rather than a `default` so that the linter will flag the
|
||||
// switch as needing an update.
|
||||
// ignore: dead_code
|
||||
return PlatformFocusMode.auto;
|
||||
}
|
||||
|
||||
/// Returns an [ExposureMode]'s Pigeon representation.
|
||||
PlatformExposureMode _pigeonExposureMode(ExposureMode mode) {
|
||||
switch (mode) {
|
||||
case ExposureMode.locked:
|
||||
return PlatformExposureMode.locked;
|
||||
case ExposureMode.auto:
|
||||
return PlatformExposureMode.auto;
|
||||
}
|
||||
// The enum comes from a different package, which could get a new value at
|
||||
// any time, so provide a fallback that ensures this won't break when used
|
||||
// with a version that contains new values. This is deliberately outside
|
||||
// the switch rather than a `default` so that the linter will flag the
|
||||
// switch as needing an update.
|
||||
// ignore: dead_code
|
||||
return PlatformExposureMode.auto;
|
||||
}
|
||||
|
||||
/// Returns a [FlashMode]'s Pigeon representation.
|
||||
PlatformFlashMode _pigeonFlashMode(FlashMode flashMode) {
|
||||
switch (flashMode) {
|
||||
case FlashMode.off:
|
||||
return PlatformFlashMode.off;
|
||||
case FlashMode.auto:
|
||||
return PlatformFlashMode.auto;
|
||||
case FlashMode.always:
|
||||
return PlatformFlashMode.always;
|
||||
case FlashMode.torch:
|
||||
return PlatformFlashMode.torch;
|
||||
}
|
||||
// The enum comes from a different package, which could get a new value at
|
||||
// any time, so provide a fallback that ensures this won't break when used
|
||||
// with a version that contains new values. This is deliberately outside
|
||||
// the switch rather than a `default` so that the linter will flag the
|
||||
// switch as needing an update.
|
||||
// ignore: dead_code
|
||||
return PlatformFlashMode.off;
|
||||
}
|
||||
|
||||
/// Returns a [ResolutionPreset]'s Pigeon representation.
|
||||
PlatformResolutionPreset _pigeonResolutionPreset(ResolutionPreset? resolutionPreset) {
|
||||
if (resolutionPreset == null) {
|
||||
// Provide a default if one isn't provided, since the native side needs
|
||||
// to set something.
|
||||
return PlatformResolutionPreset.high;
|
||||
}
|
||||
switch (resolutionPreset) {
|
||||
case ResolutionPreset.max:
|
||||
return PlatformResolutionPreset.max;
|
||||
case ResolutionPreset.ultraHigh:
|
||||
return PlatformResolutionPreset.ultraHigh;
|
||||
case ResolutionPreset.veryHigh:
|
||||
return PlatformResolutionPreset.veryHigh;
|
||||
case ResolutionPreset.high:
|
||||
return PlatformResolutionPreset.high;
|
||||
case ResolutionPreset.medium:
|
||||
return PlatformResolutionPreset.medium;
|
||||
case ResolutionPreset.low:
|
||||
return PlatformResolutionPreset.low;
|
||||
}
|
||||
// The enum comes from a different package, which could get a new value at
|
||||
// any time, so provide a fallback that ensures this won't break when used
|
||||
// with a version that contains new values. This is deliberately outside
|
||||
// the switch rather than a `default` so that the linter will flag the
|
||||
// switch as needing an update.
|
||||
// ignore: dead_code
|
||||
return PlatformResolutionPreset.max;
|
||||
}
|
||||
|
||||
/// Returns a [VideoStabilizationMode]'s Pigeon representation.
|
||||
PlatformVideoStabilizationMode? _pigeonVideoStabilizationMode(
|
||||
VideoStabilizationMode videoStabilizationMode,
|
||||
) {
|
||||
switch (videoStabilizationMode) {
|
||||
case VideoStabilizationMode.off:
|
||||
return PlatformVideoStabilizationMode.off;
|
||||
case VideoStabilizationMode.level1:
|
||||
return PlatformVideoStabilizationMode.standard;
|
||||
case VideoStabilizationMode.level2:
|
||||
return PlatformVideoStabilizationMode.cinematic;
|
||||
case VideoStabilizationMode.level3:
|
||||
return PlatformVideoStabilizationMode.cinematicExtended;
|
||||
}
|
||||
// The enum comes from a different package, which could get a new value at
|
||||
// any time, so provide a fallback that ensures this won't break when used
|
||||
// with a version that contains new values. This is deliberately outside
|
||||
// the switch rather than a `default` so that the linter will flag the
|
||||
// switch as needing an update.
|
||||
// ignore: dead_code
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns an [ImageFormatGroup]'s Pigeon representation.
|
||||
PlatformImageFormatGroup _pigeonImageFormat(ImageFormatGroup format) {
|
||||
switch (format) {
|
||||
// "unknown" is used to indicate the default.
|
||||
case ImageFormatGroup.unknown:
|
||||
case ImageFormatGroup.bgra8888:
|
||||
return PlatformImageFormatGroup.bgra8888;
|
||||
case ImageFormatGroup.yuv420:
|
||||
return PlatformImageFormatGroup.yuv420;
|
||||
case ImageFormatGroup.jpeg:
|
||||
case ImageFormatGroup.nv21:
|
||||
// Fall through.
|
||||
}
|
||||
// The enum comes from a different package, which could get a new value at
|
||||
// any time, so provide a fallback that ensures this won't break when used
|
||||
// with a version that contains new values. This is deliberately outside
|
||||
// the switch rather than a `default` so that the linter will flag the
|
||||
// switch as needing an update.
|
||||
// TODO(stuartmorgan): Consider throwing an UnsupportedError, instead of
|
||||
// doing fallback, when a specific unsupported format is requested. This
|
||||
// would require a breaking change at this layer and the app-facing layer.
|
||||
return PlatformImageFormatGroup.bgra8888;
|
||||
}
|
||||
|
||||
/// Returns an [ImageFileFormat]'s Pigeon representation.
|
||||
PlatformImageFileFormat _pigeonImageFileFormat(ImageFileFormat format) {
|
||||
switch (format) {
|
||||
case ImageFileFormat.heif:
|
||||
return PlatformImageFileFormat.heif;
|
||||
case ImageFileFormat.jpeg:
|
||||
return PlatformImageFileFormat.jpeg;
|
||||
}
|
||||
// The enum comes from a different package, which could get a new value at
|
||||
// any time, so provide a fallback that ensures this won't break when used
|
||||
// with a version that contains new values. This is deliberately outside
|
||||
// the switch rather than a `default` so that the linter will flag the
|
||||
// switch as needing an update.
|
||||
// TODO(stuartmorgan): Consider throwing an UnsupportedError, instead of
|
||||
// doing fallback, when a specific unsupported format is requested. This
|
||||
// would require a breaking change at this layer and the app-facing layer.
|
||||
// ignore: dead_code
|
||||
return PlatformImageFileFormat.jpeg;
|
||||
}
|
||||
|
||||
/// Returns a [Point]s Pigeon representation.
|
||||
PlatformPoint? _pigeonPoint(Point<double>? point) {
|
||||
if (point == null) {
|
||||
return null;
|
||||
}
|
||||
return PlatformPoint(x: point.x, y: point.y);
|
||||
}
|
||||
}
|
||||
|
||||
/// Callback handler for device-level events from the platform host.
|
||||
@visibleForTesting
|
||||
class HostDeviceMessageHandler implements CameraGlobalEventApi {
|
||||
/// Creates a new handler and registers it to listen to its platform channel.
|
||||
HostDeviceMessageHandler() {
|
||||
CameraGlobalEventApi.setUp(this);
|
||||
}
|
||||
|
||||
/// The controller used to broadcast general device events coming from the
|
||||
/// host platform.
|
||||
///
|
||||
/// It is a `broadcast` because multiple controllers will connect to
|
||||
/// different stream views of this Controller.
|
||||
final StreamController<DeviceEvent> deviceEventStreamController =
|
||||
StreamController<DeviceEvent>.broadcast();
|
||||
|
||||
@override
|
||||
void deviceOrientationChanged(PlatformDeviceOrientation orientation) {
|
||||
deviceEventStreamController.add(
|
||||
DeviceOrientationChangedEvent(deviceOrientationFromPlatform(orientation)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Callback handler for camera-level events from the platform host.
|
||||
@visibleForTesting
|
||||
class HostCameraMessageHandler implements CameraEventApi {
|
||||
/// Creates a new handler that listens for events from camera [cameraId], and
|
||||
/// broadcasts them to [streamController].
|
||||
HostCameraMessageHandler(this.cameraId, this.streamController) {
|
||||
CameraEventApi.setUp(this, messageChannelSuffix: cameraId.toString());
|
||||
}
|
||||
|
||||
/// Removes the handler for native messages.
|
||||
void dispose() {
|
||||
CameraEventApi.setUp(null, messageChannelSuffix: cameraId.toString());
|
||||
}
|
||||
|
||||
/// The camera ID this handler listens for events from.
|
||||
final int cameraId;
|
||||
|
||||
/// The controller used to broadcast camera events coming from the
|
||||
/// host platform.
|
||||
final StreamController<CameraEvent> streamController;
|
||||
|
||||
@override
|
||||
void error(String message) {
|
||||
streamController.add(CameraErrorEvent(cameraId, message));
|
||||
}
|
||||
|
||||
@override
|
||||
void initialized(PlatformCameraState initialState) {
|
||||
streamController.add(
|
||||
CameraInitializedEvent(
|
||||
cameraId,
|
||||
initialState.previewSize.width,
|
||||
initialState.previewSize.height,
|
||||
exposureModeFromPlatform(initialState.exposureMode),
|
||||
initialState.exposurePointSupported,
|
||||
focusModeFromPlatform(initialState.focusMode),
|
||||
initialState.focusPointSupported,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1400
camera_avfoundation/lib/src/messages.g.dart
Normal file
1400
camera_avfoundation/lib/src/messages.g.dart
Normal file
File diff suppressed because it is too large
Load diff
50
camera_avfoundation/lib/src/type_conversion.dart
Normal file
50
camera_avfoundation/lib/src/type_conversion.dart
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:camera_platform_interface/camera_platform_interface.dart';
|
||||
|
||||
import 'messages.g.dart';
|
||||
|
||||
/// Converts method channel call [data] for `receivedImageStreamData` to a
|
||||
/// [CameraImageData].
|
||||
CameraImageData cameraImageFromPlatformData(PlatformCameraImageData data) {
|
||||
return CameraImageData(
|
||||
format: _cameraImageFormatFromPlatformImageFormat(data.formatCode),
|
||||
width: data.width,
|
||||
height: data.height,
|
||||
lensAperture: data.lensAperture,
|
||||
sensorExposureTime: data.sensorExposureTimeNanoseconds,
|
||||
sensorSensitivity: data.sensorSensitivity,
|
||||
planes: List<CameraImagePlane>.unmodifiable(
|
||||
data.planes.map<CameraImagePlane>(
|
||||
(PlatformCameraImagePlane planeData) => _cameraImagePlaneFromPlatformData(planeData),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
CameraImageFormat _cameraImageFormatFromPlatformImageFormat(int data) {
|
||||
return CameraImageFormat(_imageFormatGroupFromPlatformImageFormat(data), raw: data);
|
||||
}
|
||||
|
||||
ImageFormatGroup _imageFormatGroupFromPlatformImageFormat(int data) {
|
||||
switch (data) {
|
||||
case 875704438: // kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
|
||||
return ImageFormatGroup.yuv420;
|
||||
|
||||
case 1111970369: // kCVPixelFormatType_32BGRA
|
||||
return ImageFormatGroup.bgra8888;
|
||||
}
|
||||
|
||||
return ImageFormatGroup.unknown;
|
||||
}
|
||||
|
||||
CameraImagePlane _cameraImagePlaneFromPlatformData(PlatformCameraImagePlane data) {
|
||||
return CameraImagePlane(
|
||||
bytes: data.bytes,
|
||||
bytesPerRow: data.bytesPerRow,
|
||||
width: data.width,
|
||||
height: data.height,
|
||||
);
|
||||
}
|
||||
84
camera_avfoundation/lib/src/utils.dart
Normal file
84
camera_avfoundation/lib/src/utils.dart
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:camera_platform_interface/camera_platform_interface.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'messages.g.dart';
|
||||
|
||||
/// Creates a [CameraDescription] from a Pigeon [PlatformCameraDescription].
|
||||
CameraDescription cameraDescriptionFromPlatform(PlatformCameraDescription camera) {
|
||||
return CameraDescription(
|
||||
name: camera.name,
|
||||
lensDirection: cameraLensDirectionFromPlatform(camera.lensDirection),
|
||||
sensorOrientation: 90,
|
||||
lensType: cameraLensTypeFromPlatform(camera.lensType),
|
||||
);
|
||||
}
|
||||
|
||||
/// Converts a Pigeon [PlatformCameraLensDirection] to a [CameraLensDirection].
|
||||
CameraLensDirection cameraLensDirectionFromPlatform(PlatformCameraLensDirection direction) {
|
||||
return switch (direction) {
|
||||
PlatformCameraLensDirection.front => CameraLensDirection.front,
|
||||
PlatformCameraLensDirection.back => CameraLensDirection.back,
|
||||
PlatformCameraLensDirection.external => CameraLensDirection.external,
|
||||
};
|
||||
}
|
||||
|
||||
/// Converts a Pigeon [PlatformCameraLensType] to a [CameraLensType].
|
||||
CameraLensType cameraLensTypeFromPlatform(PlatformCameraLensType type) {
|
||||
return switch (type) {
|
||||
PlatformCameraLensType.wide => CameraLensType.wide,
|
||||
PlatformCameraLensType.telephoto => CameraLensType.telephoto,
|
||||
PlatformCameraLensType.ultraWide => CameraLensType.ultraWide,
|
||||
PlatformCameraLensType.unknown => CameraLensType.unknown,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convents the given device orientation to Pigeon.
|
||||
PlatformDeviceOrientation serializeDeviceOrientation(DeviceOrientation orientation) {
|
||||
switch (orientation) {
|
||||
case DeviceOrientation.portraitUp:
|
||||
return PlatformDeviceOrientation.portraitUp;
|
||||
case DeviceOrientation.portraitDown:
|
||||
return PlatformDeviceOrientation.portraitDown;
|
||||
case DeviceOrientation.landscapeRight:
|
||||
return PlatformDeviceOrientation.landscapeRight;
|
||||
case DeviceOrientation.landscapeLeft:
|
||||
return PlatformDeviceOrientation.landscapeLeft;
|
||||
}
|
||||
// The enum comes from a different package, which could get a new value at
|
||||
// any time, so provide a fallback that ensures this won't break when used
|
||||
// with a version that contains new values. This is deliberately outside
|
||||
// the switch rather than a `default` so that the linter will flag the
|
||||
// switch as needing an update.
|
||||
// ignore: dead_code
|
||||
return PlatformDeviceOrientation.portraitUp;
|
||||
}
|
||||
|
||||
/// Converts a Pigeon [PlatformDeviceOrientation] to a [DeviceOrientation].
|
||||
DeviceOrientation deviceOrientationFromPlatform(PlatformDeviceOrientation orientation) {
|
||||
return switch (orientation) {
|
||||
PlatformDeviceOrientation.portraitUp => DeviceOrientation.portraitUp,
|
||||
PlatformDeviceOrientation.portraitDown => DeviceOrientation.portraitDown,
|
||||
PlatformDeviceOrientation.landscapeLeft => DeviceOrientation.landscapeLeft,
|
||||
PlatformDeviceOrientation.landscapeRight => DeviceOrientation.landscapeRight,
|
||||
};
|
||||
}
|
||||
|
||||
/// Converts a Pigeon [PlatformExposureMode] to an [ExposureMode].
|
||||
ExposureMode exposureModeFromPlatform(PlatformExposureMode mode) {
|
||||
return switch (mode) {
|
||||
PlatformExposureMode.auto => ExposureMode.auto,
|
||||
PlatformExposureMode.locked => ExposureMode.locked,
|
||||
};
|
||||
}
|
||||
|
||||
/// Converts a Pigeon [PlatformFocusMode] to an [FocusMode].
|
||||
FocusMode focusModeFromPlatform(PlatformFocusMode mode) {
|
||||
return switch (mode) {
|
||||
PlatformFocusMode.auto => FocusMode.auto,
|
||||
PlatformFocusMode.locked => FocusMode.locked,
|
||||
};
|
||||
}
|
||||
3
camera_avfoundation/pigeons/copyright.txt
Normal file
3
camera_avfoundation/pigeons/copyright.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Copyright 2013 The Flutter Authors
|
||||
Use of this source code is governed by a BSD-style license that can be
|
||||
found in the LICENSE file.
|
||||
375
camera_avfoundation/pigeons/messages.dart
Normal file
375
camera_avfoundation/pigeons/messages.dart
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:pigeon/pigeon.dart';
|
||||
|
||||
@ConfigurePigeon(
|
||||
PigeonOptions(
|
||||
dartOut: 'lib/src/messages.g.dart',
|
||||
swiftOut: 'darwin/camera_avfoundation/Sources/camera_avfoundation/Messages.swift',
|
||||
copyrightHeader: 'pigeons/copyright.txt',
|
||||
),
|
||||
)
|
||||
// Pigeon version of CameraLensDirection.
|
||||
enum PlatformCameraLensDirection {
|
||||
/// Front facing camera (a user looking at the screen is seen by the camera).
|
||||
front,
|
||||
|
||||
/// Back facing camera (a user looking at the screen is not seen by the camera).
|
||||
back,
|
||||
|
||||
/// External camera which may not be mounted to the device.
|
||||
external,
|
||||
}
|
||||
|
||||
// Pigeon version of CameraLensDirection.
|
||||
enum PlatformCameraLensType {
|
||||
/// A built-in wide-angle camera device type.
|
||||
wide,
|
||||
|
||||
/// A built-in camera device type with a longer focal length than a wide-angle camera.
|
||||
telephoto,
|
||||
|
||||
/// A built-in camera device type with a shorter focal length than a wide-angle camera.
|
||||
ultraWide,
|
||||
|
||||
/// Unknown camera device type.
|
||||
unknown,
|
||||
}
|
||||
|
||||
// Pigeon version of DeviceOrientation.
|
||||
enum PlatformDeviceOrientation { portraitUp, landscapeLeft, portraitDown, landscapeRight }
|
||||
|
||||
// Pigeon version of ExposureMode.
|
||||
enum PlatformExposureMode { auto, locked }
|
||||
|
||||
// Pigeon version of FlashMode.
|
||||
enum PlatformFlashMode { off, auto, always, torch }
|
||||
|
||||
// Pigeon version of FocusMode.
|
||||
enum PlatformFocusMode { auto, locked }
|
||||
|
||||
/// Pigeon version of ImageFileFormat.
|
||||
enum PlatformImageFileFormat { jpeg, heif }
|
||||
|
||||
// Pigeon version of the subset of ImageFormatGroup supported on iOS.
|
||||
enum PlatformImageFormatGroup { bgra8888, yuv420 }
|
||||
|
||||
// Pigeon version of ResolutionPreset.
|
||||
enum PlatformResolutionPreset { low, medium, high, veryHigh, ultraHigh, max }
|
||||
|
||||
enum PlatformVideoStabilizationMode { off, standard, cinematic, cinematicExtended }
|
||||
|
||||
// Pigeon version of CameraDescription.
|
||||
class PlatformCameraDescription {
|
||||
PlatformCameraDescription({
|
||||
required this.name,
|
||||
required this.lensDirection,
|
||||
required this.lensType,
|
||||
});
|
||||
|
||||
/// The name of the camera device.
|
||||
final String name;
|
||||
|
||||
/// The direction the camera is facing.
|
||||
final PlatformCameraLensDirection lensDirection;
|
||||
|
||||
/// The type of the camera lens.
|
||||
final PlatformCameraLensType lensType;
|
||||
}
|
||||
|
||||
// Pigeon version of the data needed for a CameraInitializedEvent.
|
||||
class PlatformCameraState {
|
||||
PlatformCameraState({
|
||||
required this.previewSize,
|
||||
required this.exposureMode,
|
||||
required this.focusMode,
|
||||
required this.exposurePointSupported,
|
||||
required this.focusPointSupported,
|
||||
});
|
||||
|
||||
/// The size of the preview, in pixels.
|
||||
final PlatformSize previewSize;
|
||||
|
||||
/// The default exposure mode
|
||||
final PlatformExposureMode exposureMode;
|
||||
|
||||
/// The default focus mode
|
||||
final PlatformFocusMode focusMode;
|
||||
|
||||
/// Whether setting exposure points is supported.
|
||||
final bool exposurePointSupported;
|
||||
|
||||
/// Whether setting focus points is supported.
|
||||
final bool focusPointSupported;
|
||||
}
|
||||
|
||||
// Pigeon version of the data needed for a CameraImageData.
|
||||
class PlatformCameraImageData {
|
||||
PlatformCameraImageData({
|
||||
required this.formatCode,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.planes,
|
||||
required this.lensAperture,
|
||||
required this.sensorExposureTimeNanoseconds,
|
||||
required this.sensorSensitivity,
|
||||
});
|
||||
|
||||
/// The FourCharCode of the image format.
|
||||
final int formatCode;
|
||||
|
||||
final int width;
|
||||
final int height;
|
||||
final List<PlatformCameraImagePlane> planes;
|
||||
final double lensAperture;
|
||||
final int sensorExposureTimeNanoseconds;
|
||||
final double sensorSensitivity;
|
||||
}
|
||||
|
||||
// Pigeon version of the data needed for a CameraImagePlane.
|
||||
class PlatformCameraImagePlane {
|
||||
const PlatformCameraImagePlane({
|
||||
required this.bytes,
|
||||
required this.bytesPerRow,
|
||||
required this.width,
|
||||
required this.height,
|
||||
});
|
||||
|
||||
final Uint8List bytes;
|
||||
final int bytesPerRow;
|
||||
final int width;
|
||||
final int height;
|
||||
}
|
||||
|
||||
// Pigeon version of to MediaSettings.
|
||||
class PlatformMediaSettings {
|
||||
PlatformMediaSettings({
|
||||
required this.resolutionPreset,
|
||||
required this.framesPerSecond,
|
||||
required this.videoBitrate,
|
||||
required this.audioBitrate,
|
||||
required this.enableAudio,
|
||||
});
|
||||
|
||||
final PlatformResolutionPreset resolutionPreset;
|
||||
final int? framesPerSecond;
|
||||
final int? videoBitrate;
|
||||
final int? audioBitrate;
|
||||
final bool enableAudio;
|
||||
}
|
||||
|
||||
// Pigeon equivalent of CGPoint.
|
||||
class PlatformPoint {
|
||||
PlatformPoint({required this.x, required this.y});
|
||||
|
||||
final double x;
|
||||
final double y;
|
||||
}
|
||||
|
||||
// Pigeon equivalent of CGSize.
|
||||
class PlatformSize {
|
||||
PlatformSize({required this.width, required this.height});
|
||||
|
||||
final double width;
|
||||
final double height;
|
||||
}
|
||||
|
||||
@HostApi()
|
||||
abstract class CameraApi {
|
||||
/// Returns the list of available cameras.
|
||||
@async
|
||||
@ObjCSelector('availableCamerasWithCompletion')
|
||||
List<PlatformCameraDescription> getAvailableCameras();
|
||||
|
||||
/// Create a new camera with the given settings, and returns its ID.
|
||||
@async
|
||||
@ObjCSelector('createCameraWithName:settings:')
|
||||
int create(String cameraName, PlatformMediaSettings settings);
|
||||
|
||||
/// Initializes the camera with the given ID.
|
||||
@async
|
||||
@ObjCSelector('initializeCamera:withImageFormat:')
|
||||
void initialize(int cameraId, PlatformImageFormatGroup imageFormat);
|
||||
|
||||
/// Begins streaming frames from the camera.
|
||||
@async
|
||||
void startImageStream();
|
||||
|
||||
/// Stops streaming frames from the camera.
|
||||
@async
|
||||
void stopImageStream();
|
||||
|
||||
/// Called by the Dart side of the plugin when it has received the last image
|
||||
/// frame sent.
|
||||
///
|
||||
/// This is used to throttle sending frames across the channel.
|
||||
@async
|
||||
void receivedImageStreamData();
|
||||
|
||||
/// Indicates that the given camera is no longer being used on the Dart side,
|
||||
/// and any associated resources can be cleaned up.
|
||||
@async
|
||||
@ObjCSelector('disposeCamera:')
|
||||
void dispose(int cameraId);
|
||||
|
||||
/// Locks the camera capture to the current device orientation.
|
||||
@async
|
||||
@ObjCSelector('lockCaptureOrientation:')
|
||||
void lockCaptureOrientation(PlatformDeviceOrientation orientation);
|
||||
|
||||
/// Unlocks camera capture orientation, allowing it to automatically adapt to
|
||||
/// device orientation.
|
||||
@async
|
||||
void unlockCaptureOrientation();
|
||||
|
||||
/// Takes a picture with the current settings, and returns the path to the
|
||||
/// resulting file.
|
||||
@async
|
||||
String takePicture();
|
||||
|
||||
/// Does any preprocessing necessary before beginning to record video.
|
||||
@async
|
||||
void prepareForVideoRecording();
|
||||
|
||||
/// Begins recording video, optionally enabling streaming to Dart at the same
|
||||
/// time.
|
||||
@async
|
||||
@ObjCSelector('startVideoRecordingWithStreaming:')
|
||||
void startVideoRecording(bool enableStream);
|
||||
|
||||
/// Stops recording video, and results the path to the resulting file.
|
||||
@async
|
||||
String stopVideoRecording();
|
||||
|
||||
/// Pauses video recording.
|
||||
@async
|
||||
void pauseVideoRecording();
|
||||
|
||||
/// Resumes a previously paused video recording.
|
||||
@async
|
||||
void resumeVideoRecording();
|
||||
|
||||
/// Switches the camera to the given flash mode.
|
||||
@async
|
||||
@ObjCSelector('setFlashMode:')
|
||||
void setFlashMode(PlatformFlashMode mode);
|
||||
|
||||
/// Switches the camera to the given exposure mode.
|
||||
@async
|
||||
@ObjCSelector('setExposureMode:')
|
||||
void setExposureMode(PlatformExposureMode mode);
|
||||
|
||||
/// Anchors auto-exposure to the given point in (0,1) coordinate space.
|
||||
///
|
||||
/// A null value resets to the default exposure point.
|
||||
@async
|
||||
@ObjCSelector('setExposurePoint:')
|
||||
void setExposurePoint(PlatformPoint? point);
|
||||
|
||||
/// Returns the minimum exposure offset supported by the camera.
|
||||
@async
|
||||
@ObjCSelector('getMinimumExposureOffset')
|
||||
double getMinExposureOffset();
|
||||
|
||||
/// Returns the maximum exposure offset supported by the camera.
|
||||
@async
|
||||
@ObjCSelector('getMaximumExposureOffset')
|
||||
double getMaxExposureOffset();
|
||||
|
||||
/// Sets the exposure offset manually to the given value.
|
||||
@async
|
||||
@ObjCSelector('setExposureOffset:')
|
||||
void setExposureOffset(double offset);
|
||||
|
||||
/// Switches the camera to the given focus mode.
|
||||
@async
|
||||
@ObjCSelector('setFocusMode:')
|
||||
void setFocusMode(PlatformFocusMode mode);
|
||||
|
||||
/// Anchors auto-focus to the given point in (0,1) coordinate space.
|
||||
///
|
||||
/// A null value resets to the default focus point.
|
||||
@async
|
||||
@ObjCSelector('setFocusPoint:')
|
||||
void setFocusPoint(PlatformPoint? point);
|
||||
|
||||
/// Returns the minimum zoom level supported by the camera.
|
||||
@async
|
||||
@ObjCSelector('getMinimumZoomLevel')
|
||||
double getMinZoomLevel();
|
||||
|
||||
/// Returns the maximum zoom level supported by the camera.
|
||||
@async
|
||||
@ObjCSelector('getMaximumZoomLevel')
|
||||
double getMaxZoomLevel();
|
||||
|
||||
/// Sets the zoom factor.
|
||||
@async
|
||||
@ObjCSelector('setZoomLevel:')
|
||||
void setZoomLevel(double zoom);
|
||||
|
||||
/// Sets the video stabilization mode.
|
||||
@async
|
||||
@ObjCSelector('setVideoStabilizationMode:')
|
||||
void setVideoStabilizationMode(PlatformVideoStabilizationMode mode);
|
||||
|
||||
/// Gets if the given video stabilization mode is supported.
|
||||
@async
|
||||
@ObjCSelector('isVideoStabilizationModeSupported:')
|
||||
bool isVideoStabilizationModeSupported(PlatformVideoStabilizationMode mode);
|
||||
|
||||
/// Pauses streaming of preview frames.
|
||||
@async
|
||||
void pausePreview();
|
||||
|
||||
/// Resumes a previously paused preview stream.
|
||||
@async
|
||||
void resumePreview();
|
||||
|
||||
/// Changes the camera used while recording video.
|
||||
///
|
||||
/// This should only be called while video recording is active.
|
||||
@async
|
||||
void updateDescriptionWhileRecording(String cameraName);
|
||||
|
||||
/// Sets the file format used for taking pictures.
|
||||
@async
|
||||
@ObjCSelector('setImageFileFormat:')
|
||||
void setImageFileFormat(PlatformImageFileFormat format);
|
||||
|
||||
/// Sets the JPEG compression quality for still image capture.
|
||||
@async
|
||||
@ObjCSelector('setJpegImageQuality:')
|
||||
void setJpegImageQuality(int quality);
|
||||
}
|
||||
|
||||
@EventChannelApi()
|
||||
abstract class CameraImageStreamEventApi {
|
||||
PlatformCameraImageData imageDataStream();
|
||||
}
|
||||
|
||||
/// Handler for native callbacks that are not tied to a specific camera ID.
|
||||
@FlutterApi()
|
||||
abstract class CameraGlobalEventApi {
|
||||
/// Called when the device's physical orientation changes.
|
||||
void deviceOrientationChanged(PlatformDeviceOrientation orientation);
|
||||
}
|
||||
|
||||
/// Handler for native callbacks that are tied to a specific camera ID.
|
||||
///
|
||||
/// This is intended to be initialized with the camera ID as a suffix.
|
||||
@FlutterApi()
|
||||
abstract class CameraEventApi {
|
||||
/// Called when the camera is inialitized for use.
|
||||
@ObjCSelector('initializedWithState:')
|
||||
void initialized(PlatformCameraState initialState);
|
||||
|
||||
/// Called when an error occurs in the camera.
|
||||
///
|
||||
/// This should be used for errors that occur outside of the context of
|
||||
/// handling a specific HostApi call, such as during streaming.
|
||||
@ObjCSelector('reportError:')
|
||||
void error(String message);
|
||||
}
|
||||
37
camera_avfoundation/pubspec.yaml
Normal file
37
camera_avfoundation/pubspec.yaml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
name: camera_avfoundation
|
||||
description: iOS implementation of the camera plugin.
|
||||
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_avfoundation
|
||||
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
|
||||
version: 0.10.3
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.0
|
||||
flutter: ">=3.38.0"
|
||||
|
||||
flutter:
|
||||
plugin:
|
||||
implements: camera
|
||||
platforms:
|
||||
ios:
|
||||
pluginClass: CameraPlugin
|
||||
dartPluginClass: AVFoundationCamera
|
||||
sharedDarwinSource: true
|
||||
|
||||
dependencies:
|
||||
camera_platform_interface: ^2.13.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
meta: ^1.10.0
|
||||
stream_transform: ^2.0.0
|
||||
|
||||
dev_dependencies:
|
||||
async: ^2.5.0
|
||||
build_runner: ^2.4.9
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
leak_tracker_flutter_testing: any
|
||||
mockito: ^5.4.4
|
||||
pigeon: ^27.3.2
|
||||
|
||||
topics:
|
||||
- camera
|
||||
798
camera_avfoundation/test/avfoundation_camera_test.dart
Normal file
798
camera_avfoundation/test/avfoundation_camera_test.dart
Normal file
|
|
@ -0,0 +1,798 @@
|
|||
// Copyright 2013 The Flutter Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:camera_avfoundation/src/avfoundation_camera.dart';
|
||||
import 'package:camera_avfoundation/src/messages.g.dart';
|
||||
import 'package:camera_avfoundation/src/utils.dart';
|
||||
import 'package:camera_platform_interface/camera_platform_interface.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import 'avfoundation_camera_test.mocks.dart';
|
||||
|
||||
@GenerateNiceMocks(<MockSpec<Object>>[MockSpec<CameraApi>()])
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test('registers instance', () async {
|
||||
AVFoundationCamera.registerWith();
|
||||
expect(CameraPlatform.instance, isA<AVFoundationCamera>());
|
||||
});
|
||||
|
||||
group('Creation, Initialization & Disposal Tests', () {
|
||||
test('Should send creation data and receive back a camera id', () async {
|
||||
// Arrange
|
||||
final mockApi = MockCameraApi();
|
||||
when(mockApi.create(any, any)).thenAnswer((_) async => 1);
|
||||
final camera = AVFoundationCamera(api: mockApi);
|
||||
const cameraName = 'Test';
|
||||
|
||||
// Act
|
||||
final int cameraId = await camera.createCamera(
|
||||
const CameraDescription(
|
||||
name: cameraName,
|
||||
lensDirection: CameraLensDirection.back,
|
||||
sensorOrientation: 0,
|
||||
),
|
||||
ResolutionPreset.high,
|
||||
);
|
||||
|
||||
// Assert
|
||||
final VerificationResult verification = verify(mockApi.create(captureAny, captureAny));
|
||||
expect(verification.captured[0], cameraName);
|
||||
final settings = verification.captured[1] as PlatformMediaSettings?;
|
||||
expect(settings, isNotNull);
|
||||
expect(settings?.resolutionPreset, PlatformResolutionPreset.high);
|
||||
expect(cameraId, 1);
|
||||
});
|
||||
|
||||
test(
|
||||
'Should send creation data and receive back a camera id using createCameraWithSettings',
|
||||
() async {
|
||||
// Arrange
|
||||
final mockApi = MockCameraApi();
|
||||
when(mockApi.create(any, any)).thenAnswer((_) async => 1);
|
||||
final camera = AVFoundationCamera(api: mockApi);
|
||||
const cameraName = 'Test';
|
||||
const fps = 15;
|
||||
const videoBitrate = 200000;
|
||||
const audioBitrate = 32000;
|
||||
|
||||
// Act
|
||||
final int cameraId = await camera.createCameraWithSettings(
|
||||
const CameraDescription(
|
||||
name: cameraName,
|
||||
lensDirection: CameraLensDirection.back,
|
||||
sensorOrientation: 0,
|
||||
),
|
||||
const MediaSettings(
|
||||
resolutionPreset: ResolutionPreset.low,
|
||||
fps: fps,
|
||||
videoBitrate: videoBitrate,
|
||||
audioBitrate: audioBitrate,
|
||||
enableAudio: true,
|
||||
),
|
||||
);
|
||||
|
||||
// Assert
|
||||
final VerificationResult verification = verify(mockApi.create(captureAny, captureAny));
|
||||
expect(verification.captured[0], cameraName);
|
||||
final settings = verification.captured[1] as PlatformMediaSettings?;
|
||||
expect(settings, isNotNull);
|
||||
expect(settings?.resolutionPreset, PlatformResolutionPreset.low);
|
||||
expect(settings?.framesPerSecond, fps);
|
||||
expect(settings?.videoBitrate, videoBitrate);
|
||||
expect(settings?.audioBitrate, audioBitrate);
|
||||
expect(settings?.enableAudio, true);
|
||||
expect(cameraId, 1);
|
||||
},
|
||||
);
|
||||
|
||||
test('Should throw CameraException when create throws a PlatformException', () {
|
||||
// Arrange
|
||||
const exceptionCode = 'TESTING_ERROR_CODE';
|
||||
const exceptionMessage = 'Mock error message used during testing.';
|
||||
final mockApi = MockCameraApi();
|
||||
when(mockApi.create(any, any)).thenAnswer((_) async {
|
||||
throw PlatformException(code: exceptionCode, message: exceptionMessage);
|
||||
});
|
||||
final camera = AVFoundationCamera(api: mockApi);
|
||||
|
||||
// Act
|
||||
expect(
|
||||
() => camera.createCamera(
|
||||
const CameraDescription(
|
||||
name: 'Test',
|
||||
lensDirection: CameraLensDirection.back,
|
||||
sensorOrientation: 0,
|
||||
),
|
||||
ResolutionPreset.high,
|
||||
),
|
||||
throwsA(
|
||||
isA<CameraException>()
|
||||
.having((CameraException e) => e.code, 'code', exceptionCode)
|
||||
.having((CameraException e) => e.description, 'description', exceptionMessage),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('Should throw CameraException when initialize throws a PlatformException', () {
|
||||
// Arrange
|
||||
const exceptionCode = 'TESTING_ERROR_CODE';
|
||||
const exceptionMessage = 'Mock error message used during testing.';
|
||||
final mockApi = MockCameraApi();
|
||||
when(mockApi.initialize(any, any)).thenAnswer((_) async {
|
||||
throw PlatformException(code: exceptionCode, message: exceptionMessage);
|
||||
});
|
||||
final camera = AVFoundationCamera(api: mockApi);
|
||||
|
||||
// Act
|
||||
expect(
|
||||
() => camera.initializeCamera(0),
|
||||
throwsA(
|
||||
isA<CameraException>()
|
||||
.having((CameraException e) => e.code, 'code', 'TESTING_ERROR_CODE')
|
||||
.having(
|
||||
(CameraException e) => e.description,
|
||||
'description',
|
||||
'Mock error message used during testing.',
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('Should send initialization data', () async {
|
||||
// Arrange
|
||||
final mockApi = MockCameraApi();
|
||||
final camera = AVFoundationCamera(api: mockApi);
|
||||
final int cameraId = await camera.createCamera(
|
||||
const CameraDescription(
|
||||
name: 'Test',
|
||||
lensDirection: CameraLensDirection.back,
|
||||
sensorOrientation: 0,
|
||||
),
|
||||
ResolutionPreset.high,
|
||||
);
|
||||
|
||||
// Act
|
||||
final Future<void> initializeFuture = camera.initializeCamera(cameraId);
|
||||
camera.cameraEventStreamController.add(
|
||||
CameraInitializedEvent(cameraId, 1920, 1080, ExposureMode.auto, true, FocusMode.auto, true),
|
||||
);
|
||||
await initializeFuture;
|
||||
|
||||
// Assert
|
||||
final VerificationResult verification = verify(mockApi.initialize(captureAny, captureAny));
|
||||
expect(verification.captured[0], cameraId);
|
||||
// The default when unspecified should be bgra8888.
|
||||
expect(verification.captured[1], PlatformImageFormatGroup.bgra8888);
|
||||
});
|
||||
|
||||
test('Should send a disposal call on dispose', () async {
|
||||
// Arrange
|
||||
final mockApi = MockCameraApi();
|
||||
final camera = AVFoundationCamera(api: mockApi);
|
||||
final int cameraId = await camera.createCamera(
|
||||
const CameraDescription(
|
||||
name: 'Test',
|
||||
lensDirection: CameraLensDirection.back,
|
||||
sensorOrientation: 0,
|
||||
),
|
||||
ResolutionPreset.high,
|
||||
);
|
||||
final Future<void> initializeFuture = camera.initializeCamera(cameraId);
|
||||
camera.cameraEventStreamController.add(
|
||||
CameraInitializedEvent(cameraId, 1920, 1080, ExposureMode.auto, true, FocusMode.auto, true),
|
||||
);
|
||||
await initializeFuture;
|
||||
|
||||
// Act
|
||||
await camera.dispose(cameraId);
|
||||
|
||||
// Assert
|
||||
final VerificationResult verification = verify(mockApi.dispose(captureAny));
|
||||
expect(verification.captured[0], cameraId);
|
||||
});
|
||||
});
|
||||
|
||||
group('Event Tests', () {
|
||||
late AVFoundationCamera camera;
|
||||
late int cameraId;
|
||||
setUp(() async {
|
||||
final mockApi = MockCameraApi();
|
||||
when(mockApi.create(any, any)).thenAnswer((_) async => 1);
|
||||
camera = AVFoundationCamera(api: mockApi);
|
||||
cameraId = await camera.createCamera(
|
||||
const CameraDescription(
|
||||
name: 'Test',
|
||||
lensDirection: CameraLensDirection.back,
|
||||
sensorOrientation: 0,
|
||||
),
|
||||
ResolutionPreset.high,
|
||||
);
|
||||
final Future<void> initializeFuture = camera.initializeCamera(cameraId);
|
||||
camera.cameraEventStreamController.add(
|
||||
CameraInitializedEvent(cameraId, 1920, 1080, ExposureMode.auto, true, FocusMode.auto, true),
|
||||
);
|
||||
await initializeFuture;
|
||||
});
|
||||
|
||||
test('Should receive initialized event', () async {
|
||||
// Act
|
||||
final Stream<CameraInitializedEvent> eventStream = camera.onCameraInitialized(cameraId);
|
||||
final streamQueue = StreamQueue<CameraInitializedEvent>(eventStream);
|
||||
|
||||
final previewSize = PlatformSize(width: 3840, height: 2160);
|
||||
// Emit test events
|
||||
final event = CameraInitializedEvent(
|
||||
cameraId,
|
||||
previewSize.width,
|
||||
previewSize.height,
|
||||
ExposureMode.auto,
|
||||
true,
|
||||
FocusMode.auto,
|
||||
true,
|
||||
);
|
||||
camera.hostCameraHandlers[cameraId]!.initialized(
|
||||
PlatformCameraState(
|
||||
previewSize: previewSize,
|
||||
exposureMode: PlatformExposureMode.auto,
|
||||
focusMode: PlatformFocusMode.auto,
|
||||
exposurePointSupported: true,
|
||||
focusPointSupported: true,
|
||||
),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(await streamQueue.next, event);
|
||||
|
||||
// Clean up
|
||||
await streamQueue.cancel();
|
||||
});
|
||||
|
||||
test('Should receive camera error events', () async {
|
||||
// Act
|
||||
final Stream<CameraErrorEvent> errorStream = camera.onCameraError(cameraId);
|
||||
final streamQueue = StreamQueue<CameraErrorEvent>(errorStream);
|
||||
|
||||
// Emit test events
|
||||
const errorMessage = 'Error Description';
|
||||
final event = CameraErrorEvent(cameraId, errorMessage);
|
||||
camera.hostCameraHandlers[cameraId]!.error(errorMessage);
|
||||
camera.hostCameraHandlers[cameraId]!.error(errorMessage);
|
||||
camera.hostCameraHandlers[cameraId]!.error(errorMessage);
|
||||
|
||||
// Assert
|
||||
expect(await streamQueue.next, event);
|
||||
expect(await streamQueue.next, event);
|
||||
expect(await streamQueue.next, event);
|
||||
|
||||
// Clean up
|
||||
await streamQueue.cancel();
|
||||
});
|
||||
|
||||
test('Should receive device orientation change events', () async {
|
||||
// Act
|
||||
final Stream<DeviceOrientationChangedEvent> eventStream = camera.onDeviceOrientationChanged();
|
||||
final streamQueue = StreamQueue<DeviceOrientationChangedEvent>(eventStream);
|
||||
|
||||
// Emit test events
|
||||
const event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp);
|
||||
for (var i = 0; i < 3; i++) {
|
||||
camera.hostHandler.deviceOrientationChanged(PlatformDeviceOrientation.portraitUp);
|
||||
}
|
||||
|
||||
// Assert
|
||||
expect(await streamQueue.next, event);
|
||||
expect(await streamQueue.next, event);
|
||||
expect(await streamQueue.next, event);
|
||||
|
||||
// Clean up
|
||||
await streamQueue.cancel();
|
||||
});
|
||||
});
|
||||
|
||||
group('Function Tests', () {
|
||||
late MockCameraApi mockApi;
|
||||
late AVFoundationCamera camera;
|
||||
late int cameraId;
|
||||
|
||||
setUp(() async {
|
||||
mockApi = MockCameraApi();
|
||||
when(mockApi.create(any, any)).thenAnswer((_) async => 1);
|
||||
camera = AVFoundationCamera(api: mockApi);
|
||||
cameraId = await camera.createCamera(
|
||||
const CameraDescription(
|
||||
name: 'Test',
|
||||
lensDirection: CameraLensDirection.back,
|
||||
sensorOrientation: 0,
|
||||
),
|
||||
ResolutionPreset.high,
|
||||
);
|
||||
final Future<void> initializeFuture = camera.initializeCamera(cameraId);
|
||||
camera.cameraEventStreamController.add(
|
||||
CameraInitializedEvent(cameraId, 1920, 1080, ExposureMode.auto, true, FocusMode.auto, true),
|
||||
);
|
||||
await initializeFuture;
|
||||
});
|
||||
|
||||
test('Should fetch CameraDescription instances for available cameras', () async {
|
||||
final returnData = <PlatformCameraDescription>[
|
||||
PlatformCameraDescription(
|
||||
name: 'Test 1',
|
||||
lensDirection: PlatformCameraLensDirection.front,
|
||||
lensType: PlatformCameraLensType.ultraWide,
|
||||
),
|
||||
PlatformCameraDescription(
|
||||
name: 'Test 2',
|
||||
lensDirection: PlatformCameraLensDirection.back,
|
||||
lensType: PlatformCameraLensType.telephoto,
|
||||
),
|
||||
];
|
||||
when(mockApi.getAvailableCameras()).thenAnswer((_) async => returnData);
|
||||
|
||||
final List<CameraDescription> cameras = await camera.availableCameras();
|
||||
|
||||
expect(cameras.length, returnData.length);
|
||||
for (var i = 0; i < returnData.length; i++) {
|
||||
expect(cameras[i].name, returnData[i].name);
|
||||
expect(
|
||||
cameras[i].lensDirection,
|
||||
cameraLensDirectionFromPlatform(returnData[i].lensDirection),
|
||||
);
|
||||
expect(cameras[i].lensType, cameraLensTypeFromPlatform(returnData[i].lensType));
|
||||
// This value isn't provided by the platform, so is hard-coded to 90.
|
||||
expect(cameras[i].sensorOrientation, 90);
|
||||
}
|
||||
});
|
||||
|
||||
test('Should throw CameraException when availableCameras throws a PlatformException', () {
|
||||
const code = 'TESTING_ERROR_CODE';
|
||||
const message = 'Mock error message used during testing.';
|
||||
when(
|
||||
mockApi.getAvailableCameras(),
|
||||
).thenAnswer((_) async => throw PlatformException(code: code, message: message));
|
||||
|
||||
expect(
|
||||
camera.availableCameras,
|
||||
throwsA(
|
||||
isA<CameraException>()
|
||||
.having((CameraException e) => e.code, 'code', code)
|
||||
.having((CameraException e) => e.description, 'description', message),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('Should take a picture and return an XFile instance', () async {
|
||||
const stubPath = '/test/path.jpg';
|
||||
when(mockApi.takePicture()).thenAnswer((_) async => stubPath);
|
||||
|
||||
final XFile file = await camera.takePicture(cameraId);
|
||||
|
||||
expect(file.path, stubPath);
|
||||
});
|
||||
|
||||
test('Should prepare for video recording', () async {
|
||||
await camera.prepareForVideoRecording();
|
||||
|
||||
verify(mockApi.prepareForVideoRecording());
|
||||
});
|
||||
|
||||
test('Should start recording a video', () async {
|
||||
await camera.startVideoRecording(cameraId);
|
||||
|
||||
verify(mockApi.startVideoRecording(any));
|
||||
});
|
||||
|
||||
test(
|
||||
'Should pass enableStream if callback is passed when starting recording a video',
|
||||
() async {
|
||||
await camera.startVideoCapturing(
|
||||
VideoCaptureOptions(cameraId, streamCallback: (CameraImageData imageData) {}),
|
||||
);
|
||||
|
||||
verify(mockApi.startVideoRecording(true));
|
||||
},
|
||||
);
|
||||
|
||||
test('Should stop a video recording and return the file', () async {
|
||||
const stubPath = '/test/path.mp4';
|
||||
when(mockApi.stopVideoRecording()).thenAnswer((_) async => stubPath);
|
||||
|
||||
final XFile file = await camera.stopVideoRecording(cameraId);
|
||||
|
||||
expect(file.path, stubPath);
|
||||
});
|
||||
|
||||
test('Should pause a video recording', () async {
|
||||
await camera.pauseVideoRecording(cameraId);
|
||||
|
||||
verify(mockApi.pauseVideoRecording());
|
||||
});
|
||||
|
||||
test('Should resume a video recording', () async {
|
||||
await camera.resumeVideoRecording(cameraId);
|
||||
|
||||
verify(mockApi.resumeVideoRecording());
|
||||
});
|
||||
|
||||
test('Should set the description while recording', () async {
|
||||
const camera2Description = CameraDescription(
|
||||
name: 'Test2',
|
||||
lensDirection: CameraLensDirection.front,
|
||||
sensorOrientation: 0,
|
||||
);
|
||||
|
||||
await camera.setDescriptionWhileRecording(camera2Description);
|
||||
|
||||
verify(mockApi.updateDescriptionWhileRecording(camera2Description.name));
|
||||
});
|
||||
|
||||
test('Should set the flash mode to torch', () async {
|
||||
await camera.setFlashMode(cameraId, FlashMode.torch);
|
||||
|
||||
verify(mockApi.setFlashMode(PlatformFlashMode.torch));
|
||||
});
|
||||
|
||||
test('Should set the flash mode to always', () async {
|
||||
await camera.setFlashMode(cameraId, FlashMode.always);
|
||||
|
||||
verify(mockApi.setFlashMode(PlatformFlashMode.always));
|
||||
});
|
||||
|
||||
test('Should set the flash mode to auto', () async {
|
||||
await camera.setFlashMode(cameraId, FlashMode.auto);
|
||||
|
||||
verify(mockApi.setFlashMode(PlatformFlashMode.auto));
|
||||
});
|
||||
|
||||
test('Should set the flash mode to off', () async {
|
||||
await camera.setFlashMode(cameraId, FlashMode.off);
|
||||
|
||||
verify(mockApi.setFlashMode(PlatformFlashMode.off));
|
||||
});
|
||||
|
||||
test('Should set the exposure mode to auto', () async {
|
||||
await camera.setExposureMode(cameraId, ExposureMode.auto);
|
||||
|
||||
verify(mockApi.setExposureMode(PlatformExposureMode.auto));
|
||||
});
|
||||
|
||||
test('Should set the exposure mode to locked', () async {
|
||||
await camera.setExposureMode(cameraId, ExposureMode.locked);
|
||||
|
||||
verify(mockApi.setExposureMode(PlatformExposureMode.locked));
|
||||
});
|
||||
|
||||
test('Should set the exposure point to a value', () async {
|
||||
const point = Point<double>(0.4, 0.6);
|
||||
await camera.setExposurePoint(cameraId, point);
|
||||
|
||||
final VerificationResult verification = verify(mockApi.setExposurePoint(captureAny));
|
||||
final passedPoint = verification.captured[0] as PlatformPoint?;
|
||||
expect(passedPoint?.x, point.x);
|
||||
expect(passedPoint?.y, point.y);
|
||||
});
|
||||
|
||||
test('Should set the exposure point to null for reset', () async {
|
||||
await camera.setExposurePoint(cameraId, null);
|
||||
|
||||
final VerificationResult verification = verify(mockApi.setExposurePoint(captureAny));
|
||||
final passedPoint = verification.captured[0] as PlatformPoint?;
|
||||
expect(passedPoint, null);
|
||||
});
|
||||
|
||||
test('Should get the min exposure offset', () async {
|
||||
const stubMinOffset = 2.0;
|
||||
when(mockApi.getMinExposureOffset()).thenAnswer((_) async => stubMinOffset);
|
||||
|
||||
final double minExposureOffset = await camera.getMinExposureOffset(cameraId);
|
||||
|
||||
expect(minExposureOffset, stubMinOffset);
|
||||
});
|
||||
|
||||
test('Should get the max exposure offset', () async {
|
||||
const stubMaxOffset = 2.0;
|
||||
when(mockApi.getMaxExposureOffset()).thenAnswer((_) async => stubMaxOffset);
|
||||
|
||||
final double maxExposureOffset = await camera.getMaxExposureOffset(cameraId);
|
||||
|
||||
expect(maxExposureOffset, stubMaxOffset);
|
||||
});
|
||||
|
||||
test('Exposure offset step size should always return zero', () async {
|
||||
final double stepSize = await camera.getExposureOffsetStepSize(cameraId);
|
||||
|
||||
expect(stepSize, 0.0);
|
||||
});
|
||||
|
||||
test('Should set the exposure offset', () async {
|
||||
const stubOffset = 0.5;
|
||||
final double actualOffset = await camera.setExposureOffset(cameraId, 0.5);
|
||||
|
||||
verify(mockApi.setExposureOffset(stubOffset));
|
||||
// iOS never adjusts the offset.
|
||||
expect(actualOffset, stubOffset);
|
||||
});
|
||||
|
||||
test('Should set the focus mode to auto', () async {
|
||||
await camera.setFocusMode(cameraId, FocusMode.auto);
|
||||
|
||||
verify(mockApi.setFocusMode(PlatformFocusMode.auto));
|
||||
});
|
||||
|
||||
test('Should set the focus mode to locked', () async {
|
||||
await camera.setFocusMode(cameraId, FocusMode.locked);
|
||||
|
||||
verify(mockApi.setFocusMode(PlatformFocusMode.locked));
|
||||
});
|
||||
|
||||
test('Should set the focus point to a value', () async {
|
||||
const point = Point<double>(0.4, 0.6);
|
||||
await camera.setFocusPoint(cameraId, point);
|
||||
|
||||
final VerificationResult verification = verify(mockApi.setFocusPoint(captureAny));
|
||||
final passedPoint = verification.captured[0] as PlatformPoint?;
|
||||
expect(passedPoint?.x, point.x);
|
||||
expect(passedPoint?.y, point.y);
|
||||
});
|
||||
|
||||
test('Should set the focus point to null for reset', () async {
|
||||
await camera.setFocusPoint(cameraId, null);
|
||||
|
||||
final VerificationResult verification = verify(mockApi.setFocusPoint(captureAny));
|
||||
final passedPoint = verification.captured[0] as PlatformPoint?;
|
||||
expect(passedPoint, null);
|
||||
});
|
||||
|
||||
test('Should build a texture widget as preview widget', () async {
|
||||
final Widget widget = camera.buildPreview(cameraId);
|
||||
|
||||
expect(widget is Texture, isTrue);
|
||||
expect((widget as Texture).textureId, cameraId);
|
||||
});
|
||||
|
||||
test('Should get the max zoom level', () async {
|
||||
const stubZoomLevel = 10.0;
|
||||
when(mockApi.getMaxZoomLevel()).thenAnswer((_) async => stubZoomLevel);
|
||||
|
||||
final double maxZoomLevel = await camera.getMaxZoomLevel(cameraId);
|
||||
|
||||
expect(maxZoomLevel, stubZoomLevel);
|
||||
});
|
||||
|
||||
test('Should get the min zoom level', () async {
|
||||
const stubZoomLevel = 10.0;
|
||||
when(mockApi.getMinZoomLevel()).thenAnswer((_) async => stubZoomLevel);
|
||||
|
||||
final double minZoomLevel = await camera.getMinZoomLevel(cameraId);
|
||||
|
||||
expect(minZoomLevel, stubZoomLevel);
|
||||
});
|
||||
|
||||
test('Should set the zoom level', () async {
|
||||
const zoom = 2.0;
|
||||
|
||||
await camera.setZoomLevel(cameraId, zoom);
|
||||
|
||||
verify(mockApi.setZoomLevel(zoom));
|
||||
});
|
||||
|
||||
test('Should throw CameraException when illegal zoom level is supplied', () async {
|
||||
const code = 'ZOOM_ERROR';
|
||||
const message = 'Illegal zoom error';
|
||||
when(
|
||||
mockApi.setZoomLevel(any),
|
||||
).thenAnswer((_) async => throw PlatformException(code: code, message: message));
|
||||
|
||||
expect(
|
||||
() => camera.setZoomLevel(cameraId, -1.0),
|
||||
throwsA(
|
||||
isA<CameraException>()
|
||||
.having((CameraException e) => e.code, 'code', code)
|
||||
.having((CameraException e) => e.description, 'description', message),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('Should set video stabilization mode to off', () async {
|
||||
when(
|
||||
mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.off),
|
||||
).thenAnswer((_) async => true);
|
||||
|
||||
await camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.off);
|
||||
|
||||
verify(mockApi.setVideoStabilizationMode(PlatformVideoStabilizationMode.off));
|
||||
});
|
||||
|
||||
test('Should set video stabilization mode to level1', () async {
|
||||
when(
|
||||
mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.standard),
|
||||
).thenAnswer((_) async => true);
|
||||
|
||||
await camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level1);
|
||||
|
||||
verify(mockApi.setVideoStabilizationMode(PlatformVideoStabilizationMode.standard));
|
||||
});
|
||||
|
||||
test('Should set video stabilization mode to cinematic', () async {
|
||||
when(
|
||||
mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.cinematic),
|
||||
).thenAnswer((_) async => true);
|
||||
|
||||
await camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level2);
|
||||
|
||||
verify(mockApi.setVideoStabilizationMode(PlatformVideoStabilizationMode.cinematic));
|
||||
});
|
||||
|
||||
test('Should set video stabilization mode to cinematicExtended', () async {
|
||||
when(
|
||||
mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.cinematicExtended),
|
||||
).thenAnswer((_) async => true);
|
||||
|
||||
await camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level3);
|
||||
|
||||
verify(mockApi.setVideoStabilizationMode(PlatformVideoStabilizationMode.cinematicExtended));
|
||||
});
|
||||
|
||||
test('Should get no video stabilization mode', () async {
|
||||
when(mockApi.isVideoStabilizationModeSupported(any)).thenAnswer((_) async => false);
|
||||
|
||||
final Iterable<VideoStabilizationMode> modes = await camera
|
||||
.getSupportedVideoStabilizationModes(cameraId);
|
||||
|
||||
expect(modes, isEmpty);
|
||||
});
|
||||
|
||||
test('Should get off and standard video stabilization modes', () async {
|
||||
when(
|
||||
mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.off),
|
||||
).thenAnswer((_) async => true);
|
||||
when(
|
||||
mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.standard),
|
||||
).thenAnswer((_) async => true);
|
||||
when(
|
||||
mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.cinematic),
|
||||
).thenAnswer((_) async => false);
|
||||
when(
|
||||
mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.cinematicExtended),
|
||||
).thenAnswer((_) async => false);
|
||||
|
||||
final List<VideoStabilizationMode> modes = (await camera.getSupportedVideoStabilizationModes(
|
||||
cameraId,
|
||||
)).toList();
|
||||
|
||||
expect(modes, <VideoStabilizationMode>[
|
||||
VideoStabilizationMode.off,
|
||||
VideoStabilizationMode.level1,
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should get all video stabilization modes', () async {
|
||||
when(mockApi.isVideoStabilizationModeSupported(any)).thenAnswer((_) async => true);
|
||||
|
||||
final List<VideoStabilizationMode> modes = (await camera.getSupportedVideoStabilizationModes(
|
||||
cameraId,
|
||||
)).toList();
|
||||
|
||||
expect(modes, <VideoStabilizationMode>[
|
||||
VideoStabilizationMode.off,
|
||||
VideoStabilizationMode.level1,
|
||||
VideoStabilizationMode.level2,
|
||||
VideoStabilizationMode.level3,
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should throw ArgumentError when unavailable video stabilization mode is set', () async {
|
||||
when(mockApi.isVideoStabilizationModeSupported(any)).thenAnswer((_) async => false);
|
||||
|
||||
expect(
|
||||
() => camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.off),
|
||||
throwsA(isA<ArgumentError>().having((ArgumentError e) => e.name, 'name', 'mode')),
|
||||
);
|
||||
expect(
|
||||
() => camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level1),
|
||||
throwsA(isA<ArgumentError>().having((ArgumentError e) => e.name, 'name', 'mode')),
|
||||
);
|
||||
expect(
|
||||
() => camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level2),
|
||||
throwsA(isA<ArgumentError>().having((ArgumentError e) => e.name, 'name', 'mode')),
|
||||
);
|
||||
expect(
|
||||
() => camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level3),
|
||||
throwsA(isA<ArgumentError>().having((ArgumentError e) => e.name, 'name', 'mode')),
|
||||
);
|
||||
});
|
||||
|
||||
test('Should throw CameraException when illegal zoom level is supplied', () async {
|
||||
const code = 'ZOOM_ERROR';
|
||||
const message = 'Illegal zoom error';
|
||||
when(
|
||||
mockApi.setZoomLevel(any),
|
||||
).thenAnswer((_) async => throw PlatformException(code: code, message: message));
|
||||
|
||||
expect(
|
||||
() => camera.setZoomLevel(cameraId, -1.0),
|
||||
throwsA(
|
||||
isA<CameraException>()
|
||||
.having((CameraException e) => e.code, 'code', code)
|
||||
.having((CameraException e) => e.description, 'description', message),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('Should lock the capture orientation', () async {
|
||||
await camera.lockCaptureOrientation(cameraId, DeviceOrientation.portraitUp);
|
||||
|
||||
verify(mockApi.lockCaptureOrientation(PlatformDeviceOrientation.portraitUp));
|
||||
});
|
||||
|
||||
test('Should unlock the capture orientation', () async {
|
||||
await camera.unlockCaptureOrientation(cameraId);
|
||||
|
||||
verify(mockApi.unlockCaptureOrientation());
|
||||
});
|
||||
|
||||
test('Should pause the camera preview', () async {
|
||||
await camera.pausePreview(cameraId);
|
||||
|
||||
verify(mockApi.pausePreview());
|
||||
});
|
||||
|
||||
test('Should resume the camera preview', () async {
|
||||
await camera.resumePreview(cameraId);
|
||||
|
||||
verify(mockApi.resumePreview());
|
||||
});
|
||||
|
||||
test('Should report support for image streaming', () async {
|
||||
expect(camera.supportsImageStreaming(), true);
|
||||
});
|
||||
|
||||
test('Should start streaming', () async {
|
||||
final StreamSubscription<CameraImageData> subscription = camera
|
||||
.onStreamedFrameAvailable(cameraId)
|
||||
.listen((CameraImageData imageData) {});
|
||||
|
||||
verify(mockApi.startImageStream());
|
||||
|
||||
await subscription.cancel();
|
||||
});
|
||||
|
||||
test('Should stop streaming', () async {
|
||||
final StreamSubscription<CameraImageData> subscription = camera
|
||||
.onStreamedFrameAvailable(cameraId)
|
||||
.listen((CameraImageData imageData) {});
|
||||
await subscription.cancel();
|
||||
|
||||
verify(mockApi.startImageStream());
|
||||
verify(mockApi.stopImageStream());
|
||||
});
|
||||
|
||||
test('Should set the ImageFileFormat to heif', () async {
|
||||
await camera.setImageFileFormat(cameraId, ImageFileFormat.heif);
|
||||
|
||||
verify(mockApi.setImageFileFormat(PlatformImageFileFormat.heif));
|
||||
});
|
||||
|
||||
test('Should set the ImageFileFormat to jpeg', () async {
|
||||
await camera.setImageFileFormat(cameraId, ImageFileFormat.jpeg);
|
||||
|
||||
verify(mockApi.setImageFileFormat(PlatformImageFileFormat.jpeg));
|
||||
});
|
||||
|
||||
test('Should set the image quality', () async {
|
||||
await camera.setJpegImageQuality(cameraId, 50);
|
||||
|
||||
verify(mockApi.setJpegImageQuality(50));
|
||||
});
|
||||
});
|
||||
}
|
||||
354
camera_avfoundation/test/avfoundation_camera_test.mocks.dart
Normal file
354
camera_avfoundation/test/avfoundation_camera_test.mocks.dart
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
// Mocks generated by Mockito 5.4.6 from annotations
|
||||
// in camera_avfoundation/test/avfoundation_camera_test.dart.
|
||||
// Do not manually edit this file.
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _i4;
|
||||
|
||||
import 'package:camera_avfoundation/src/messages.g.dart' as _i2;
|
||||
import 'package:mockito/mockito.dart' as _i1;
|
||||
import 'package:mockito/src/dummies.dart' as _i3;
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: avoid_redundant_argument_values
|
||||
// ignore_for_file: avoid_setters_without_getters
|
||||
// ignore_for_file: comment_references
|
||||
// ignore_for_file: deprecated_member_use
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||
// ignore_for_file: must_be_immutable
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
// ignore_for_file: unnecessary_parenthesis
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: subtype_of_sealed_class
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
/// A class which mocks [CameraApi].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockCameraApi extends _i1.Mock implements _i2.CameraApi {
|
||||
@override
|
||||
String get pigeonVar_messageChannelSuffix =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#pigeonVar_messageChannelSuffix),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#pigeonVar_messageChannelSuffix),
|
||||
),
|
||||
returnValueForMissingStub: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#pigeonVar_messageChannelSuffix),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
_i4.Future<List<_i2.PlatformCameraDescription>> getAvailableCameras() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getAvailableCameras, []),
|
||||
returnValue: _i4.Future<List<_i2.PlatformCameraDescription>>.value(
|
||||
<_i2.PlatformCameraDescription>[],
|
||||
),
|
||||
returnValueForMissingStub: _i4.Future<List<_i2.PlatformCameraDescription>>.value(
|
||||
<_i2.PlatformCameraDescription>[],
|
||||
),
|
||||
)
|
||||
as _i4.Future<List<_i2.PlatformCameraDescription>>);
|
||||
|
||||
@override
|
||||
_i4.Future<int> create(String? cameraName, _i2.PlatformMediaSettings? settings) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#create, [cameraName, settings]),
|
||||
returnValue: _i4.Future<int>.value(0),
|
||||
returnValueForMissingStub: _i4.Future<int>.value(0),
|
||||
)
|
||||
as _i4.Future<int>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> initialize(int? cameraId, _i2.PlatformImageFormatGroup? imageFormat) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#initialize, [cameraId, imageFormat]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> startImageStream() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#startImageStream, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> stopImageStream() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#stopImageStream, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> receivedImageStreamData() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#receivedImageStreamData, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> dispose(int? cameraId) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#dispose, [cameraId]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> lockCaptureOrientation(_i2.PlatformDeviceOrientation? orientation) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#lockCaptureOrientation, [orientation]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> unlockCaptureOrientation() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#unlockCaptureOrientation, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<String> takePicture() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#takePicture, []),
|
||||
returnValue: _i4.Future<String>.value(
|
||||
_i3.dummyValue<String>(this, Invocation.method(#takePicture, [])),
|
||||
),
|
||||
returnValueForMissingStub: _i4.Future<String>.value(
|
||||
_i3.dummyValue<String>(this, Invocation.method(#takePicture, [])),
|
||||
),
|
||||
)
|
||||
as _i4.Future<String>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> prepareForVideoRecording() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#prepareForVideoRecording, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> startVideoRecording(bool? enableStream) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#startVideoRecording, [enableStream]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<String> stopVideoRecording() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#stopVideoRecording, []),
|
||||
returnValue: _i4.Future<String>.value(
|
||||
_i3.dummyValue<String>(this, Invocation.method(#stopVideoRecording, [])),
|
||||
),
|
||||
returnValueForMissingStub: _i4.Future<String>.value(
|
||||
_i3.dummyValue<String>(this, Invocation.method(#stopVideoRecording, [])),
|
||||
),
|
||||
)
|
||||
as _i4.Future<String>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> pauseVideoRecording() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#pauseVideoRecording, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> resumeVideoRecording() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#resumeVideoRecording, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setFlashMode(_i2.PlatformFlashMode? mode) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setFlashMode, [mode]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setExposureMode(_i2.PlatformExposureMode? mode) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setExposureMode, [mode]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setExposurePoint(_i2.PlatformPoint? point) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setExposurePoint, [point]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<double> getMinExposureOffset() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getMinExposureOffset, []),
|
||||
returnValue: _i4.Future<double>.value(0.0),
|
||||
returnValueForMissingStub: _i4.Future<double>.value(0.0),
|
||||
)
|
||||
as _i4.Future<double>);
|
||||
|
||||
@override
|
||||
_i4.Future<double> getMaxExposureOffset() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getMaxExposureOffset, []),
|
||||
returnValue: _i4.Future<double>.value(0.0),
|
||||
returnValueForMissingStub: _i4.Future<double>.value(0.0),
|
||||
)
|
||||
as _i4.Future<double>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setExposureOffset(double? offset) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setExposureOffset, [offset]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setFocusMode(_i2.PlatformFocusMode? mode) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setFocusMode, [mode]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setFocusPoint(_i2.PlatformPoint? point) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setFocusPoint, [point]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<double> getMinZoomLevel() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getMinZoomLevel, []),
|
||||
returnValue: _i4.Future<double>.value(0.0),
|
||||
returnValueForMissingStub: _i4.Future<double>.value(0.0),
|
||||
)
|
||||
as _i4.Future<double>);
|
||||
|
||||
@override
|
||||
_i4.Future<double> getMaxZoomLevel() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#getMaxZoomLevel, []),
|
||||
returnValue: _i4.Future<double>.value(0.0),
|
||||
returnValueForMissingStub: _i4.Future<double>.value(0.0),
|
||||
)
|
||||
as _i4.Future<double>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setZoomLevel(double? zoom) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setZoomLevel, [zoom]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setVideoStabilizationMode(_i2.PlatformVideoStabilizationMode? mode) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setVideoStabilizationMode, [mode]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<bool> isVideoStabilizationModeSupported(_i2.PlatformVideoStabilizationMode? mode) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#isVideoStabilizationModeSupported, [mode]),
|
||||
returnValue: _i4.Future<bool>.value(false),
|
||||
returnValueForMissingStub: _i4.Future<bool>.value(false),
|
||||
)
|
||||
as _i4.Future<bool>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> pausePreview() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#pausePreview, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> resumePreview() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#resumePreview, []),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> updateDescriptionWhileRecording(String? cameraName) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#updateDescriptionWhileRecording, [cameraName]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setImageFileFormat(_i2.PlatformImageFileFormat? format) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setImageFileFormat, [format]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
|
||||
@override
|
||||
_i4.Future<void> setJpegImageQuality(int? quality) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#setJpegImageQuality, [quality]),
|
||||
returnValue: _i4.Future<void>.value(),
|
||||
returnValueForMissingStub: _i4.Future<void>.value(),
|
||||
)
|
||||
as _i4.Future<void>);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue