Commit 993892be authored by Duong Viet Anh's avatar Duong Viet Anh

init

parents
Pipeline #14623 failed with stages
File added
# 1.4.0
- ✨ Add utilities to convert AnalysisImage into JPEG in order to display them using `toJpeg()`.
- ✨ Add `preview()` and `analysisOnly()` constructors to `CameraAwesomeBuilder`.
- ✨ Volume button trigger to take picture or record/stop video.
- ✨🍏 Add brightness exposure level on iOS / iPadOS.
- 💥 AnalysisConfig has changed slightly its parameters to have platform-specific setup.
- 💥 Storage permission is now optional on Android since the introduction of `preview()`
and `analysisOnly()` modes.
- 🐛🍏 iOS / iPadOS max zoom limit.
- 🐛🤖 Better handle use cases conflicts (video + image analysis on lower-end devices) for Android.
# 1.3.1
- 🐛 Fix video recording overlay image.
- 📝 Update README.md (change feature showcase image & fix broken links).
# 1.3.0
- ✨ Customize the built-in UI by setting an `AwesomeTheme`.
- ✨ Top, middle and bottom parts of `CameraAwesomeBuilder.awesome()` can now be replaced by your
own.
- ✨ Ability to set camera preview alignment and padding.
- ✨ Ability to set aspect ratio, zoom, flash mode and SensorType when switching between front and back
camera.
- ✨ Enable/disable front camera mirroring.
- ⬆️ Upgrade `image` dependency.
- 🐛 Fix aspect ratio changes animation.
- 🐛 Smoother flash mode changes (Android).
- 🐛 Fix microphone permission (iOS).
- 🐛 Fix recorded video orientation (iOS).
- 🐛 Fix initial aspect ratio not set (iOS).
- 📝 Updated documentation and more examples.
- 🎨 Format code.
# 1.2.1
- Expose Gradle variables to avoid conflict with other plugins.
- iOS aspect ratio fix.
# 1.2.0
- Add filters for photo mode.
- Rework UI for awesome layout.
- Add start and stop method for image analysis.
- **BREAKING** Location and audio recording permissions are now optional. Add them to your
AndroidManifest manually if you need them.
- Fix preview aspectRatio on iOS.
# 1.1.0
- Use [**pigeon**](https://pub.dev/packages/pigeon) for iOS instead of classic method channel.
- Greatly improve performances on analysis mode when FPS limit disabled.
- Fix barcode scrolling to bottom.
- Fix iOS stream guards.
# 1.0.0+4
- Code formatting and linter
# 1.0.0
- Bugfixes (imageAnalysis, initialAspectRatio...)
- Sensor type switching (iOS)
- Improve AI documentation
- Add `previewSize` and `previewRect` to `CameraAwesomeBuilder` builders
# 1.0.0-rc1
- Full rework of the API
- Better feature parity between iOS and Android
- Use the built-in camera UI or make your own
- Add docs.page documentation
# 0.4.0
- Migrate to CameraX instead of Camera2 on Android.
- Add GPS location in Exif photo on Android.
- Add Video recording for Android.
# 0.3.6
- Add GPS location in Exif photo on iOS.
- Fix some issues
# 0.3.4
- Add pinch to zoom.
# 0.3.3
- update android build tools to 30
- fix first permission request crash
# 0.3.2
- Update to Flutter 3.
- Update Android example project.
- Upgrade dependencies.
- Clean some code.
# 0.3.1
- handle app lifecycle (stop camera on background)
# 0.3.0
- Migrate null safety.
- Fixed aspect ratio of camera preview when using smaller image sizes.
- Fixed image capture on older android devices which use continuous (passive) focus.
- Fix image capture on iOS
# 0.2.1+1
- build won't show red screen in debug if camerAwesome is running on slow phones
- [Android] bind activity
# 0.2.1
- [iOS] image stream available to use MLkit or other image live processing
- [iOS] code refactoring
# 0.2.0
- [iOS] video recording support
- [iOS] thread and perf enhancements
# 0.1.2+1
- [Android] onDetachedFromActivity : fix stopping the camera should be only done if camera has been started
- listen native Orientation should be canceled correctly on dispose CameraAwesomeState
- unlock focus now restart session correctly after taking a photo
- takePicture listener now cannot send result more than one time
# 0.1.2
- [Android] get luminosity level from device
- [Android] apply brightness correction
# 0.1.1+1
- [android] fix release onOpenListener after emit result to Flutter platform
# 0.1.1
- prevent starting camera when already open on Flutter side
- stability between rebuilds improved on Flutter side
- [android] check size is correctly set before starting camera
- CameraPreview try 3 times to start if camera is locked (each try are 1s ellapsed)
- Fix android zoom when taking picture
# 0.1.0
- image stream available to use MLkit or other image live processing (Only android)
# 0.0.2+3
- fix switch camera on Android with new update (now correctly switch ImageReader and cameraCharacteristics when switch sensor).
# 0.0.2+1
- comment com.google.gms.google-services from example build.gradle.
This is aimed only to start our e2e tests on testlabs. Put your own google-services.json if you want to start them there.
# 0.0.2
- updated readme
# 0.0.1
- first version. See readme for complete features list
# Contributing to CamerAwesome
## Reporting issues
You can easily report issues using GitHub.
Please include maximum information like:
- 🎯 Summary of the issue.
- 🚶‍♂️ Steps to reproduce.
- 😃 What you expected would happen.
- 🤔 What actually happens.
- 📝 Notes (possibly including why you think this might be happening, or stuff you tried that didn't work).
## Creating a Pull Request
### Code reviews
All submissions, including submissions by project members, require review.
### Match the actual coding style
Please use the actual project coding style.
- 2 spaces for indentation.
- Use Line Feed **LF**.
- Run ```flutter analyze``` before submit.
## License
By contributing, you agree that your contributions will be licensed under [CamerAwesome's licence](https://github.com/Apparence-io/camera_awesome/blob/master/LICENSE).
\ No newline at end of file
Copyright © Apparence.io
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The Software is provided “as is”, without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the Software.
\ No newline at end of file
This diff is collapsed.
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
always_use_package_imports: true
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
#Wed Apr 03 08:46:38 ICT 2024
gradle.version=7.4.2
group 'com.apparence.camerawesome'
version '1.0'
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
def DEFAULT_PLAY_SERVICES_LOCATION_VERSION = "21.0.1"
def DEFAULT_EXIF_INTERFACE_VERSION = "1.3.5"
def DEFAULT_COMPILE_SDK_VERSION = 33
def DEFAULT_MIN_SDK_VERSION = 21
def DEFAULT_MEDIA_VERSION = "1.6.0"
def getMajor(versionTab) {
versionTab == null ? null : versionTab[0].split("\\-")[0]
}
def getMinor(versionTab) {
versionTab == null ? null : versionTab.length > 1 ? versionTab[1].split("\\-")[0] : null
}
def getPatch(versionTab) {
versionTab == null ? null : versionTab.length > 2 ? versionTab[2].split("\\-")[0] : null
}
def isWithinRange(value, min, max) {
if (value == null && min == null && max == null) {
return true
} else if (value == null) {
return false
}
if (min != null && max != null) {
return (min..max).contains(value)
} else if (min != null) {
return value >= min
} else if (max != null) {
return value <= max
} else {
return true
}
}
def isVersionInRange(version, min, max) {
if (version == null) {
return false
}
def tabVersion = version.toString().split("\\.")
def tabMin = min == null ? null : min.toString().split("\\.")
def tabMax = max == null ? null : max.toString().split("\\.")
return isWithinRange(getMajor(tabVersion), getMajor(tabMin), getMajor(tabMax))
&& isWithinRange(getMinor(tabVersion), getMinor(tabMin), getMinor(tabMax))
&& isWithinRange(getPatch(tabVersion), getPatch(tabMin), getPatch(tabMax))
}
def compatibleVersion(prop, fallbackVersion, min = null, max = null) {
if (rootProject.ext.has(prop) && isVersionInRange(rootProject.ext.get(prop), min, max)) {
return rootProject.ext.get(prop)
} else {
if (rootProject.ext.has(prop)) {
println("************************** CamerAwesome **************************")
println("${prop} ${rootProject.ext.get(prop)} is not compatible with the plugin.")
if (min != null && max != null) {
println("Please use a version between ${min} and ${max}.")
} else if (min != null) {
println("Please use a version >= ${min}.")
} else if (max != null) {
println("Please use a version <= ${max}.")
}
println("Using fallback version ${fallbackVersion}.")
println("******************************************************************")
}
return fallbackVersion
}
}
android {
compileSdkVersion compatibleVersion('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION).toInteger()
defaultConfig {
minSdkVersion compatibleVersion('minSdkVersion', DEFAULT_MIN_SDK_VERSION, 21).toInteger()
}
lintOptions {
disable 'InvalidPackage'
}
testOptions {
unitTests.returnDefaultValues = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
}
dependencies {
implementation 'io.reactivex.rxjava3:rxjava:3.0.4'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'androidx.test:rules:1.5.0'
// implementation project(path: ':integration_test')
def compatPlayServicesLocationVersion = compatibleVersion('playServicesLocationVersion', DEFAULT_PLAY_SERVICES_LOCATION_VERSION)
implementation "com.google.android.gms:play-services-location:$compatPlayServicesLocationVersion"
def compatExifInterfaceVersion = compatibleVersion('compatExifInterfaceVersion', DEFAULT_EXIF_INTERFACE_VERSION)
implementation "androidx.exifinterface:exifinterface:$compatExifInterfaceVersion"
testImplementation 'junit:junit:4.13.2'
// Optional -- Mockito framework
testImplementation "org.mockito:mockito-core:4.8.0"
// Optional -- mockito-kotlin
testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
def camerax_version = "1.2.2"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"
def compatMediaVersion = compatibleVersion('compatMediaVersion', DEFAULT_MEDIA_VERSION)
implementation "androidx.media:media:${compatMediaVersion}"
implementation 'com.google.guava:guava:31.0.1-android'
}
\ No newline at end of file
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true
#Wed Nov 16 21:50:57 CET 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
{}
\ No newline at end of file
rootProject.name = 'camerawesome'
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.apparence.camerawesome">
<uses-permission android:name="android.permission.CAMERA" />
<application>
<service android:name=".buttons.PlayerService" />
</application>
</manifest>
package com.apparence.camerawesome;
/**
* CamerawesomePlugin
* This plugin recquire android Lolipop version (21) as a min version in your Android's gradle build
* */
public class CamerawesomePlugin {
public static final String TAG = CamerawesomePlugin.class.getName();
}
package com.apparence.camerawesome.buttons
import android.os.Handler
import android.os.Looper
import android.os.Message
import io.flutter.plugin.common.EventChannel
class PhysicalButtonsHandler : EventChannel.StreamHandler {
private var sink: EventChannel.EventSink? = null
fun buttonPressed(buttonId: Int) {
when (buttonId) {
VOLUME_DOWN -> {
sink?.success("VOLUME_DOWN")
}
VOLUME_UP -> {
sink?.success("VOLUME_UP")
}
}
}
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
sink = events
}
override fun onCancel(arguments: Any?) {
if (sink != null) {
sink?.endOfStream()
sink = null
}
}
companion object {
const val BROADCAST_VOLUME_BUTTONS = "BROADCAST_VOLUME_BUTTONS"
const val VOLUME_DOWN = 0
const val VOLUME_UP = 1
}
}
class PhysicalButtonMessageHandler(private val buttonsHandler: PhysicalButtonsHandler) :
Handler(Looper.getMainLooper()) {
override fun handleMessage(message: Message) {
buttonsHandler.buttonPressed(message.arg1)
}
}
\ No newline at end of file
package com.apparence.camerawesome.buttons
import android.app.Service
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.os.Message
import android.os.Messenger
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.media.VolumeProviderCompat
class PlayerService : Service() {
private var mediaSession: MediaSessionCompat? = null
private var messenger: Messenger? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
messenger =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) intent!!.extras!!.getParcelable(
PhysicalButtonsHandler.BROADCAST_VOLUME_BUTTONS, Messenger::class.java
)!!
else intent!!.extras!!.getParcelable(PhysicalButtonsHandler.BROADCAST_VOLUME_BUTTONS)!!
return super.onStartCommand(intent, flags, startId)
}
override fun onCreate() {
super.onCreate()
mediaSession = MediaSessionCompat(this, "PlayerService")
mediaSession?.setPlaybackState(
PlaybackStateCompat.Builder().setState(
PlaybackStateCompat.STATE_PLAYING, 0, 0f
) // Simulate a player which plays something.
.build()
)
val myVolumeProvider: VolumeProviderCompat = object : VolumeProviderCompat(
VOLUME_CONTROL_RELATIVE,
100, /*max volume*/
50 /*initial volume level*/
) {
override fun onAdjustVolume(direction: Int) {
/*
-1 -- volume down
1 -- volume up
0 -- volume button released
*/
if (direction < 0) {
messenger?.send(Message.obtain().apply {
arg1 = PhysicalButtonsHandler.VOLUME_DOWN
})
} else if (direction > 0) {
messenger?.send(Message.obtain().apply {
arg1 = PhysicalButtonsHandler.VOLUME_UP
})
}
}
}
mediaSession?.setPlaybackToRemote(myVolumeProvider)
mediaSession?.isActive = true
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onDestroy() {
super.onDestroy()
mediaSession?.release()
}
}
\ No newline at end of file
package com.apparence.camerawesome.cameraX
import android.graphics.ImageFormat
import android.graphics.Rect
import android.graphics.YuvImage
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import kotlin.math.min
class AnalysisImageConverter : AnalysisImageUtils {
override fun nv21toJpeg(
nv21Image: AnalysisImageWrapper,
jpegQuality: Long,
callback: (Result<AnalysisImageWrapper>) -> Unit
) {
val out = ByteArrayOutputStream()
val yuv = YuvImage(
nv21Image.bytes, ImageFormat.NV21,
nv21Image.width.toInt(), nv21Image.height.toInt(),
// TODO strides might not always be null
null
)
val success = yuv.compressToJpeg(
Rect(
nv21Image.cropRect?.left?.toInt() ?: 0, nv21Image.cropRect?.top?.toInt() ?: 0,
nv21Image.cropRect?.width?.toInt() ?: nv21Image.width.toInt(),
nv21Image.cropRect?.height?.toInt() ?: nv21Image.height.toInt(),
),
jpegQuality.toInt(), out
)
if (!success) {
callback(
Result.failure(
Exception(
"YuvImage failed to encode jpeg."
)
)
)
}
callback(
Result.success(
AnalysisImageWrapper(
bytes = out.toByteArray(),
width = nv21Image.width,
height = nv21Image.height,
cropRect = nv21Image.cropRect,
format = AnalysisImageFormat.JPEG,
planes = null,
rotation = nv21Image.rotation
)
)
)
}
override fun yuv420toJpeg(
yuvImage: AnalysisImageWrapper,
jpegQuality: Long,
callback: (Result<AnalysisImageWrapper>) -> Unit
) {
yuv420toNv21(yuvImage) { result ->
result.onSuccess {
nv21toJpeg(it, jpegQuality, callback)
}
result.onFailure {
callback(Result.failure(it))
}
}
// Below code throws the following:
// java.lang.IllegalArgumentException: only support ImageFormat.NV21 and ImageFormat.YUY2 for now
// val allPlanes = ByteArrayOutputStream()
// yuvImage.planes?.forEach { plane ->
// plane?.let {
// allPlanes.write(it.bytes)
// }
// }
//
// val out = ByteArrayOutputStream()
// val yuv = YuvImage(
// allPlanes.toByteArray(), ImageFormat.YUV_420_888,
// yuvImage.width.toInt(), yuvImage.height.toInt(),
// // TODO strides might not always be null
// yuvImage.planes?.map { it!!.bytesPerRow.toInt() }?.toIntArray()
// )
// val success = yuv.compressToJpeg(
// Rect(
// yuvImage.cropRect?.left?.toInt() ?: 0, yuvImage.cropRect?.top?.toInt() ?: 0,
// yuvImage.cropRect?.width?.toInt() ?: yuvImage.width.toInt(),
// yuvImage.cropRect?.height?.toInt() ?: yuvImage.height.toInt(),
// ),
// jpegQuality.toInt(), out
// )
// if (!success) {
// callback(
// Result.failure<AnalysisImageWrapper>(
// Exception(
// "YuvImage failed to encode jpeg."
// )
// )
// )
// }
// callback(
// Result.success(
// AnalysisImageWrapper(
// bytes = out.toByteArray(),
// width = yuvImage.width,
// height = yuvImage.height,
// cropRect = yuvImage.cropRect,
// format = AnalysisImageFormat.JPEG,
// planes = null
// )
// )
// )
}
override fun yuv420toNv21(
yuvImage: AnalysisImageWrapper,
callback: (Result<AnalysisImageWrapper>) -> Unit
) {
val yPlane = yuvImage.planes!![0]!!
val uPlane = yuvImage.planes[1]!!
val vPlane = yuvImage.planes[2]!!
val yBuffer = ByteBuffer.wrap(yPlane.bytes)
val uBuffer = ByteBuffer.wrap(uPlane.bytes)
val vBuffer = ByteBuffer.wrap(vPlane.bytes)
yBuffer.rewind()
uBuffer.rewind()
vBuffer.rewind()
val ySize = yBuffer.remaining()
var position = 0
val nv21 = ByteArray(ySize + yuvImage.width.toInt() * yuvImage.height.toInt() / 2)
// Add the full y buffer to the array. If rowStride > 1, some padding may be skipped.
// Add the full y buffer to the array. If rowStride > 1, some padding may be skipped.
for (row in 0 until yuvImage.height) {
yBuffer[nv21, position, yuvImage.width.toInt()]
position += yuvImage.width.toInt()
yBuffer.position(
min(ySize, yBuffer.position() - yuvImage.width.toInt() + yPlane.bytesPerRow.toInt())
)
}
val chromaHeight: Int = yuvImage.height.toInt() / 2
val chromaWidth: Int = yuvImage.width.toInt() / 2
val vRowStride = vPlane.bytesPerRow.toInt()
val uRowStride = uPlane.bytesPerRow.toInt()
val vPixelStride = vPlane.bytesPerPixel!!.toInt()
val uPixelStride = uPlane.bytesPerPixel!!.toInt()
// Interleave the u and v frames, filling up the rest of the buffer. Use two line buffers to
// perform faster bulk gets from the byte buffers.
// Interleave the u and v frames, filling up the rest of the buffer. Use two line buffers to
// perform faster bulk gets from the byte buffers.
val vLineBuffer = ByteArray(vRowStride)
val uLineBuffer = ByteArray(uRowStride)
for (row in 0 until chromaHeight) {
vBuffer[vLineBuffer, 0, min(vRowStride, vBuffer.remaining())]
uBuffer[uLineBuffer, 0, Math.min(uRowStride, uBuffer.remaining())]
var vLineBufferPosition = 0
var uLineBufferPosition = 0
for (col in 0 until chromaWidth) {
nv21[position++] = vLineBuffer[vLineBufferPosition]
nv21[position++] = uLineBuffer[uLineBufferPosition]
vLineBufferPosition += vPixelStride
uLineBufferPosition += uPixelStride
}
}
callback(
Result.success(
AnalysisImageWrapper(
bytes = nv21,
width = yuvImage.width,
height = yuvImage.height,
cropRect = yuvImage.cropRect,
format = AnalysisImageFormat.NV21,
planes = null,
rotation = yuvImage.rotation
)
)
)
}
override fun bgra8888toJpeg(
bgra8888image: AnalysisImageWrapper,
jpegQuality: Long,
callback: (Result<AnalysisImageWrapper>) -> Unit
) {
callback(Result.failure(Exception("BGRA 8888 conversion not implemented on Android")))
}
}
\ No newline at end of file
package com.apparence.camerawesome.cameraX
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Matrix
import android.hardware.display.DisplayManager
import android.util.Size
import android.view.Display
import android.view.Surface
import android.view.TextureView
import android.view.View
import androidx.camera.core.Preview
import androidx.camera.core.impl.PreviewConfig
import java.lang.ref.WeakReference
import java.util.*
import kotlin.math.roundToInt
/**
* Builder for [Preview] that takes in a [WeakReference] of the view finder and
* [PreviewConfig], then instantiates a [Preview] which automatically
* resizes and rotates reacting to config changes.
*
* credits to yevhenRoman
* https://gist.github.com/yevhenRoman/90681822adef43350844464be95d23f1
*/
@SuppressLint("RestrictedApi")
class AutoFitPreviewBuilder private constructor(
config: PreviewConfig,
viewFinderRef: WeakReference<TextureView>
) {
/** Public instance of preview use-case which can be used by consumers of this adapter */
val useCase: Preview
/** Internal variable used to keep track of the use-case's output rotation */
private var bufferRotation: Int = 0
/** Internal variable used to keep track of the view's rotation */
private var viewFinderRotation: Int? = null
/** Internal variable used to keep track of the use-case's output dimension */
private var bufferDimens: Size = Size(0, 0)
/** Internal variable used to keep track of the view's dimension */
private var viewFinderDimens: Size = Size(0, 0)
/** Internal variable used to keep track of the view's display */
private var viewFinderDisplay: Int = -1
private lateinit var displayManager: DisplayManager
/** We need a display listener for 180 degree device orientation changes */
private val displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) = Unit
override fun onDisplayRemoved(displayId: Int) = Unit
override fun onDisplayChanged(displayId: Int) {
val viewFinder = viewFinderRef.get() ?: return
if (displayId != viewFinderDisplay) {
val display = displayManager.getDisplay(displayId)
val rotation = getDisplaySurfaceRotation(display)
updateTransform(viewFinder, rotation, bufferDimens, viewFinderDimens)
}
}
}
init {
// Make sure that the view finder reference is valid
val viewFinder = viewFinderRef.get() ?: throw IllegalArgumentException(
"Invalid reference to view finder used"
)
// Initialize the display and rotation from texture view information
viewFinderDisplay = viewFinder.display.displayId
viewFinderRotation = getDisplaySurfaceRotation(viewFinder.display) ?: 0
// Initialize public use-case with the given config
useCase = Preview.Builder
.fromConfig(config)
.build();
// Every time the view finder is updated, recompute layout
// useCase.onPreviewOutputUpdateListener = Preview.OnPreviewOutputUpdateListener {
// val viewFinder =
// viewFinderRef.get() ?: return@OnPreviewOutputUpdateListener
//
// // To update the SurfaceTexture, we have to remove it and re-add it
// val parent = viewFinder.parent as ViewGroup
// parent.removeView(viewFinder)
// parent.addView(viewFinder, 0)
//
// viewFinder.surfaceTexture = it.surfaceTexture
// bufferRotation = it.rotationDegrees
// val rotation = getDisplaySurfaceRotation(viewFinder.display)
// updateTransform(viewFinder, rotation, it.textureSize, viewFinderDimens)
// }
// Every time the provided texture view changes, recompute layout
viewFinder.addOnLayoutChangeListener { view, left, top, right, bottom, _, _, _, _ ->
val viewFinder = view as TextureView
val newViewFinderDimens = Size(right - left, bottom - top)
val rotation = getDisplaySurfaceRotation(viewFinder.display)
updateTransform(viewFinder, rotation, bufferDimens, newViewFinderDimens)
}
// Every time the orientation of device changes, recompute layout
displayManager = viewFinder.context
.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
displayManager.registerDisplayListener(displayListener, null)
// Remove the display listeners when the view is detached to avoid
// holding a reference to the View outside of a Fragment.
// NOTE: Even though using a weak reference should take care of this,
// we still try to avoid unnecessary calls to the listener this way.
viewFinder.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(view: View) {}
override fun onViewDetachedFromWindow(view: View) {
displayManager.unregisterDisplayListener(displayListener)
}
})
}
/** Helper function that fits a camera preview into the given [TextureView] */
private fun updateTransform(
textureView: TextureView?, rotation: Int?, newBufferDimens: Size,
newViewFinderDimens: Size
) {
// This should happen anyway, but now the linter knows
val textureView = textureView ?: return
if (rotation == viewFinderRotation &&
Objects.equals(newBufferDimens, bufferDimens) &&
Objects.equals(newViewFinderDimens, viewFinderDimens)
) {
// Nothing has changed, no need to transform output again
return
}
if (rotation == null) {
// Invalid rotation - wait for valid inputs before setting matrix
return
} else {
// Update internal field with new inputs
viewFinderRotation = rotation
}
if (newBufferDimens.width == 0 || newBufferDimens.height == 0) {
// Invalid buffer dimens - wait for valid inputs before setting matrix
return
} else {
// Update internal field with new inputs
bufferDimens = newBufferDimens
}
if (newViewFinderDimens.width == 0 || newViewFinderDimens.height == 0) {
// Invalid view finder dimens - wait for valid inputs before setting matrix
return
} else {
// Update internal field with new inputs
viewFinderDimens = newViewFinderDimens
}
val matrix = Matrix()
// Compute the center of the view finder
val centerX = viewFinderDimens.width / 2f
val centerY = viewFinderDimens.height / 2f
// Correct preview output to account for display rotation
matrix.postRotate(-viewFinderRotation!!.toFloat(), centerX, centerY)
// Buffers are rotated relative to the device's 'natural' orientation: swap width and height
val bufferRatio = bufferDimens.height / bufferDimens.width.toFloat()
val scaledWidth: Int
val scaledHeight: Int
// Match longest sides together -- i.e. apply center-crop transformation
if (viewFinderDimens.width > viewFinderDimens.height) {
scaledWidth = viewFinderDimens.width
scaledHeight = (viewFinderDimens.width / bufferRatio).roundToInt()
} else {
scaledHeight = viewFinderDimens.height
scaledWidth = Math.round(viewFinderDimens.height * bufferRatio)
}
// Compute the relative scale value
val xScale = scaledWidth / viewFinderDimens.width.toFloat()
val yScale = scaledHeight / viewFinderDimens.height.toFloat()
// Scale input buffers to fill the view finder
matrix.preScale(xScale, yScale, centerX, centerY)
// Finally, apply transformations to our TextureView
textureView.setTransform(matrix)
}
companion object {
/** Helper function that gets the rotation of a [Display] in degrees */
fun getDisplaySurfaceRotation(display: Display?) = when (display?.rotation) {
Surface.ROTATION_0 -> 0
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
else -> null
}
/**
* Main entrypoint for users of this class: instantiates the adapter and returns an instance
* of [Preview] which automatically adjusts in size and rotation to compensate for
* config changes.
*/
fun build(config: PreviewConfig, viewFinder: TextureView) =
AutoFitPreviewBuilder(config, WeakReference(viewFinder)).useCase
}
}
\ No newline at end of file
package com.apparence.camerawesome.cameraX
import android.hardware.camera2.CameraCharacteristics
import android.os.Build
import android.util.Log
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
import androidx.camera.core.CameraSelector
import androidx.camera.lifecycle.ProcessCameraProvider
class CameraCapabilities {
companion object {
@androidx.annotation.OptIn(ExperimentalCamera2Interop::class)
fun getCameraLevel(
cameraSelector: CameraSelector,
cameraProvider: ProcessCameraProvider
): Int {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return cameraSelector.filter(cameraProvider.availableCameraInfos).firstOrNull()
?.let { Camera2CameraInfo.from(it) }
?.getCameraCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
?: CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
}
return CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
}
}
}
\ No newline at end of file
package com.apparence.camerawesome.cameraX
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.apparence.camerawesome.exceptions.PermissionNotDeclaredException
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class CameraPermissions : EventChannel.StreamHandler, RequestPermissionsResultListener {
private var permissionGranted = false
private var events: EventSink? = null
private var callbacks: MutableList<PermissionRequest> = mutableListOf()
// ---------------------------------------------
// EventChannel.StreamHandler
// ---------------------------------------------
override fun onListen(arguments: Any?, events: EventSink?) {
this.events = events
}
override fun onCancel(arguments: Any?) {
if (events != null) {
events!!.endOfStream()
events = null
}
}
// ---------------------------------------------
// PluginRegistry.RequestPermissionsResultListener
// ---------------------------------------------
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults: IntArray
): Boolean {
val grantedPermissions = mutableListOf<String>()
val deniedPermissions = mutableListOf<String>()
permissionGranted = true
for (i in permissions.indices) {
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
grantedPermissions.add(permissions[i])
} else {
permissionGranted = false
deniedPermissions.add(permissions[i])
}
}
val toRemove = mutableListOf<PermissionRequest>()
for (c in callbacks) {
if (c.permissionsAsked.containsAll(permissions.toList()) && permissions.toList()
.containsAll(c.permissionsAsked)
) {
c.callback(grantedPermissions, deniedPermissions)
toRemove.add(c)
}
}
callbacks.removeAll(toRemove)
if (events != null) {
Log.d(
TAG,
"_onRequestPermissionsResult: granted " + java.lang.String.join(", ", *permissions)
)
events!!.success(permissionGranted)
} else {
Log.d(
TAG, "_onRequestPermissionsResult: received permissions but the EventSink is closed"
)
}
return permissionGranted
}
fun requestBasePermissions(
activity: Activity,
saveGps: Boolean,
recordAudio: Boolean,
callback: (granted: List<String>) -> Unit
) {
val declared = declaredCameraPermissions(activity)
// Remove declared permissions not required now
if (!saveGps) {
declared.remove(Manifest.permission.ACCESS_FINE_LOCATION)
declared.remove(Manifest.permission.ACCESS_COARSE_LOCATION)
}
if (!recordAudio) {
declared.remove(Manifest.permission.RECORD_AUDIO)
}
// Throw exception if permission not declared but required here
if (saveGps && !declared.contains(Manifest.permission.ACCESS_FINE_LOCATION)) {
throw PermissionNotDeclaredException(Manifest.permission.ACCESS_FINE_LOCATION)
}
if (saveGps && !declared.contains(Manifest.permission.ACCESS_COARSE_LOCATION)) {
throw PermissionNotDeclaredException(Manifest.permission.ACCESS_COARSE_LOCATION)
}
if (recordAudio && !declared.contains(Manifest.permission.RECORD_AUDIO)) {
throw PermissionNotDeclaredException(Manifest.permission.RECORD_AUDIO)
}
// Check if some of the permissions have already been given
val permissionsToAsk: MutableList<String> = ArrayList()
val permissionsGranted: MutableList<String> = ArrayList()
for (permission in declared) {
if (ContextCompat.checkSelfPermission(
activity, permission
) != PackageManager.PERMISSION_GRANTED
) {
permissionsToAsk.add(permission)
} else {
permissionsGranted.add(permission)
}
}
permissionGranted = permissionsToAsk.size == 0
if (permissionsToAsk.isEmpty()) {
callback(permissionsGranted)
} else {
// Request the not granted permissions
CoroutineScope(Dispatchers.IO).launch {
requestPermissions(activity, permissionsToAsk, PERMISSIONS_MULTIPLE_REQUEST) {
callback(permissionsGranted.apply { addAll(it) })
}
}
}
}
/**
* Returns the list of declared camera related permissions
*/
private fun declaredCameraPermissions(context: Context): MutableList<String> {
val packageInfo = context.packageManager.getPackageInfo(
context.packageName, PackageManager.GET_PERMISSIONS
)
val permissions = packageInfo.requestedPermissions
val declaredPermissions = mutableListOf<String>()
if (permissions.isNullOrEmpty()) return declaredPermissions
for (perm in permissions) {
if (allPermissions.contains(perm)) {
declaredPermissions.add(perm)
}
}
return declaredPermissions
}
fun hasPermission(activity: Activity, permissions: List<String>): Boolean {
var granted = true
for (p in permissions) {
if (ContextCompat.checkSelfPermission(
activity, p
) != PackageManager.PERMISSION_GRANTED
) {
granted = false
break
}
}
return granted
}
suspend fun requestPermissions(
activity: Activity,
permissions: List<String>,
requestCode: Int,
callback: (denied: List<String>) -> Unit
) {
val result: List<String> = suspendCoroutine { continuation: Continuation<List<String>> ->
ActivityCompat.requestPermissions(
activity, permissions.toTypedArray(), requestCode
)
callbacks.add(
PermissionRequest(UUID.randomUUID().toString(),
permissions,
callback = { granted, _ ->
continuation.resume(granted)
})
)
}
callback(result)
}
companion object {
private val TAG = CameraPermissions::class.java.name
const val PERMISSIONS_MULTIPLE_REQUEST = 550
const val PERMISSION_GEOLOC = 560
const val PERMISSION_RECORD_AUDIO = 570
val allPermissions = listOf(
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
)
}
}
data class PermissionRequest(
var id: String,
val permissionsAsked: List<String>,
val callback: (permissionsGranted: List<String>, permissionsDenied: List<String>) -> Unit
) {}
\ No newline at end of file
package com.apparence.camerawesome.cameraX
import android.annotation.SuppressLint
import android.graphics.Rect
import android.util.Size
import androidx.camera.core.AspectRatio
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.internal.utils.ImageUtil
import com.apparence.camerawesome.utils.ResettableCountDownLatch
import io.flutter.plugin.common.EventChannel
import kotlinx.coroutines.*
import java.util.concurrent.Executor
import kotlin.math.roundToInt
import kotlin.math.roundToLong
enum class OutputImageFormat {
JPEG, YUV_420_888, NV21,
}
class ImageAnalysisBuilder private constructor(
private val format: OutputImageFormat,
private val width: Int,
private val height: Int,
private val executor: Executor,
var previewStreamSink: EventChannel.EventSink? = null,
private val maxFramesPerSecond: Double?,
) {
private var lastImageEmittedTimeStamp: Long? = null
private var countDownLatch = ResettableCountDownLatch(1)
fun lastFrameAnalysisFinished() {
countDownLatch.countDown()
}
companion object {
fun configure(
aspectRatio: Int,
format: OutputImageFormat,
executor: Executor,
width: Long?,
maxFramesPerSecond: Double?,
): ImageAnalysisBuilder {
var widthOrDefault = 1024
if (width != null && width > 0) {
widthOrDefault = width.toInt()
}
val analysisAspectRatio = when (aspectRatio) {
AspectRatio.RATIO_4_3 -> 4f / 3
else -> 16f / 9
}
val height = widthOrDefault * (1 / analysisAspectRatio)
val maxFps = if (maxFramesPerSecond == 0.0) null else maxFramesPerSecond
return ImageAnalysisBuilder(
format,
widthOrDefault,
height.toInt(),
executor,
maxFramesPerSecond = maxFps,
)
}
}
@SuppressLint("RestrictedApi")
fun build(): ImageAnalysis {
countDownLatch.reset()
val imageAnalysis = ImageAnalysis.Builder().setTargetResolution(Size(width, height))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888).build()
imageAnalysis.setAnalyzer(Dispatchers.IO.asExecutor()) { imageProxy ->
if (previewStreamSink == null) {
return@setAnalyzer
}
when (format) {
OutputImageFormat.JPEG -> {
val jpegImage = ImageUtil.yuvImageToJpegByteArray(
imageProxy,
Rect(0, 0, imageProxy.width, imageProxy.height),
80,
// imageProxy.imageInfo.rotationDegrees
)
val imageMap = imageProxyBaseAdapter(imageProxy)
imageMap["jpegImage"] = jpegImage
imageMap["cropRect"] = cropRect(imageProxy)
executor.execute { previewStreamSink?.success(imageMap) }
}
OutputImageFormat.YUV_420_888 -> {
val planes = imagePlanesAdapter(imageProxy)
val imageMap = imageProxyBaseAdapter(imageProxy)
imageMap["planes"] = planes
imageMap["cropRect"] = cropRect(imageProxy)
executor.execute { previewStreamSink?.success(imageMap) }
}
OutputImageFormat.NV21 -> {
val nv21Image = ImageUtil.yuv_420_888toNv21(imageProxy)
val planes = imagePlanesAdapter(imageProxy)
val imageMap = imageProxyBaseAdapter(imageProxy)
imageMap["nv21Image"] = nv21Image
imageMap["planes"] = planes
imageMap["cropRect"] = cropRect(imageProxy)
executor.execute { previewStreamSink?.success(imageMap) }
}
}
CoroutineScope(Dispatchers.IO).launch {
maxFramesPerSecond?.let {
if (lastImageEmittedTimeStamp == null) {
delay((1000 / it).roundToLong())
} else {
delay(
(1000 / it).roundToInt() - (System.currentTimeMillis() - lastImageEmittedTimeStamp!!)
)
}
}
countDownLatch.await()
imageProxy.close()
}
lastImageEmittedTimeStamp = System.currentTimeMillis()
}
return imageAnalysis
}
private fun cropRect(imageProxy: ImageProxy): Map<String, Any> {
return mapOf(
"left" to imageProxy.cropRect.left,
"top" to imageProxy.cropRect.top,
"right" to imageProxy.cropRect.right,
"bottom" to imageProxy.cropRect.bottom,
)
}
@SuppressLint("RestrictedApi", "UnsafeOptInUsageError")
private fun imageProxyBaseAdapter(imageProxy: ImageProxy): MutableMap<String, Any> {
return mutableMapOf(
"height" to imageProxy.image!!.height,
"width" to imageProxy.image!!.width,
"format" to format.name.lowercase(),
"rotation" to "rotation${imageProxy.imageInfo.rotationDegrees}deg",
)
}
@SuppressLint("RestrictedApi", "UnsafeOptInUsageError")
private fun imagePlanesAdapter(imageProxy: ImageProxy): List<Map<String, Any>> {
return imageProxy.image!!.planes.map {
val byteArray = ByteArray(it.buffer.remaining())
it.buffer.get(byteArray, 0, byteArray.size)
mapOf(
"bytes" to byteArray, "rowStride" to it.rowStride, "pixelStride" to it.pixelStride
)
}
}
}
\ No newline at end of file
package com.apparence.camerawesome.cameraX
import android.app.Activity
import android.view.OrientationEventListener
import android.view.Surface
import com.apparence.camerawesome.sensors.SensorOrientation
class OrientationStreamListener(
activity: Activity,
private var listeners: List<SensorOrientation>
) {
var currentOrientation: Int = 0
val surfaceOrientation
get() = when (currentOrientation) {
in 225 until 315 -> {
Surface.ROTATION_0
}
in 135 until 225 -> {
Surface.ROTATION_0
}
in 45 until 135 -> {
Surface.ROTATION_0
}
else -> {
Surface.ROTATION_0
}
}
init {
val orientationEventListener: OrientationEventListener =
object : OrientationEventListener(activity.applicationContext) {
override fun onOrientationChanged(i: Int) {
if (i == ORIENTATION_UNKNOWN) {
return
}
currentOrientation = (i + 45) / 90 * 90
if (currentOrientation == 360) currentOrientation = 0
for (listener in listeners) {
listener.onOrientationChanged(currentOrientation)
}
}
}
orientationEventListener.enable()
}
}
\ No newline at end of file
package com.apparence.camerawesome.exceptions;
public class CameraManagerException extends Exception {
public enum Codes {
MISSING_PERMISSION,
INTERRUPTED,
CANNOT_OPEN_CAMERA,
LOCKED
}
public CameraManagerException() {
}
public CameraManagerException(Codes code) {
super(code.name());
}
public CameraManagerException(Codes code, Throwable cause) {
super(code.name(), cause);
}
public CameraManagerException(Throwable cause) {
super(cause);
}
}
package com.apparence.camerawesome.exceptions;
public class CameraPreviewException extends Exception {
public enum Codes {
EMPTY_SIZE
}
public CameraPreviewException() {
}
public CameraPreviewException(Codes code) {
super(code.name());
}
public CameraPreviewException(Codes code, Throwable cause) {
super(code.name(), cause);
}
public CameraPreviewException(Throwable cause) {
super(cause);
}
}
package com.apparence.camerawesome.exceptions
class PermissionNotDeclaredException(permission: String) :
Exception("Permission not declared: $permission\nAdd it to your AndroidManifest.xml:\n<uses-permission android:name=\"$permission\" />") {
}
\ No newline at end of file
package com.apparence.camerawesome.image;
import android.media.Image;
import android.media.ImageReader;
public interface ImgConverter {
byte[] process(ImageReader imageReader);
}
package com.apparence.camerawesome.image;
import android.media.ImageReader;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
public class ImgConverterThreaded {
private static HandlerThread handlerThread = new HandlerThread("ImgConverterThreaded");
private ImgConverter converter;
public ImgConverterThreaded(ImgConverter converter) {
if(handlerThread != null) {
handlerThread.quit();
handlerThread = new HandlerThread("ImgConverterThreaded");
}
this.converter = converter;
handlerThread.start();
}
public void process(final ImageReader imageReader, final Consumer consumer) {
Looper looper = handlerThread.getLooper();
if(looper == null) {
return;
}
Handler handler = new Handler(looper);
handler.post(new Runnable() {
@Override
public void run() {
consumer.process(converter.process(imageReader));
}
});
}
public void dispose() {
handlerThread.quitSafely();
}
public interface Consumer {
void process(byte[] result);
}
}
package com.apparence.camerawesome.image;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.media.Image;
import android.media.ImageReader;
import android.os.Build;
import androidx.annotation.RequiresApi;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public
class YuvToJpgConverter implements ImgConverter{
@Override
public byte[] process(ImageReader reader) {
final Image image = reader.acquireLatestImage();
byte[] data = null;
if (image != null) {
Image.Plane[] planes = image.getPlanes();
if (image.getFormat() == ImageFormat.JPEG) {
ByteBuffer buffer = planes[0].getBuffer();
data = new byte[buffer.capacity()];
buffer.get(data);
return data;
} else if (image.getFormat() == ImageFormat.YUV_420_888) {
data = NV21toJPEG(
YUV_420_888toI420SemiPlanar(
planes[0].getBuffer(),
planes[1].getBuffer(),
planes[2].getBuffer(),
image.getWidth(), image.getHeight(),
false),
image.getWidth(), image.getHeight(), 80);
}
image.close();
}
return data;
}
public byte[] NV21toJPEG(byte[] nv21, int width, int height, int quality) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
YuvImage yuv = new YuvImage(nv21, ImageFormat.NV21, width, height, null);
yuv.compressToJpeg(new Rect(0, 0, width, height), quality, out);
return out.toByteArray();
}
// nv12: true = NV12, false = NV21
public byte[] YUV_420_888toNV(ByteBuffer yBuffer, ByteBuffer uBuffer, ByteBuffer vBuffer, boolean nv12) {
byte[] nv;
int ySize = yBuffer.remaining();
int uSize = uBuffer.remaining();
int vSize = vBuffer.remaining();
nv = new byte[ySize + uSize + vSize];
yBuffer.get(nv, 0, ySize);
if (nv12) {//U and V are swapped
vBuffer.get(nv, ySize, vSize);
uBuffer.get(nv, ySize + vSize, uSize);
} else {
uBuffer.get(nv, ySize , uSize);
vBuffer.get(nv, ySize + uSize, vSize);
}
return nv;
}
public byte[] YUV_420_888toI420SemiPlanar(ByteBuffer yBuffer, ByteBuffer uBuffer, ByteBuffer vBuffer,
int width, int height, boolean deInterleaveUV) {
byte[] data = YUV_420_888toNV(yBuffer, uBuffer, vBuffer, deInterleaveUV);
int size = width * height;
if (deInterleaveUV) {
byte[] buffer = new byte[3 * width * height / 2];
// De-interleave U and V
for (int i = 0; i < size / 4; i += 1) {
buffer[i] = data[size + 2 * i + 1];
buffer[size / 4 + i] = data[size + 2 * i];
}
System.arraycopy(buffer, 0, data, size, size / 2);
} else {
for (int i = size; i < data.length; i += 2) {
byte b1 = data[i];
data[i] = data[i + 1];
data[i + 1] = b1;
}
}
return data;
}
}
package com.apparence.camerawesome.models;
import android.graphics.Rect;
import android.hardware.camera2.CameraCharacteristics;
import android.util.Range;
import android.util.Rational;
public class CameraCharacteristicsModel {
private float maxZoom;
private Rect availablePreviewZone;
private boolean hasAutoFocus;
private Boolean flashAvailable;
private Range<Integer> aeCompensationRange;
private Rational aeCompensationRatio;
public CameraCharacteristicsModel(float maxZoom, Rect availablePreviewZone, boolean hasAutoFocus, boolean hasFlash,
Range<Integer> aeCompensationRange, Rational aeCompensationRatio) {
this.maxZoom = maxZoom;
this.availablePreviewZone = availablePreviewZone;
this.hasAutoFocus = hasAutoFocus;
this.flashAvailable = hasFlash;
this.aeCompensationRange = aeCompensationRange;
this.aeCompensationRatio = aeCompensationRatio;
}
public float getMaxZoom() {
return maxZoom;
}
public Boolean hasFlashAvailable() { return flashAvailable; }
public boolean hasAutoFocus() { return hasAutoFocus; }
public Rect getAvailablePreviewZone() {
return availablePreviewZone;
}
public Range<Integer> getAeCompensationRange() { return aeCompensationRange; }
public Rational getAeCompensationRatio() { return aeCompensationRatio; }
public static class Builder {
private float maxZoom;
private Rect availablePreviewZone;
private boolean hasAutoFocus;
private Boolean flashAvailable;
private Rational aeCompensationRatio;
private Range<Integer> aeCompensationRange;
public Builder() {}
public Builder withMaxZoom(float maxZoom) {
this.maxZoom = maxZoom;
return this;
}
public Builder withAvailablePreviewZone(Rect availablePreviewZone) {
this.availablePreviewZone = availablePreviewZone;
return this;
}
public Builder withAutoFocus(int[] modes) {
if (modes == null || modes.length == 0
|| (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) {
this.hasAutoFocus = false;
} else {
this.hasAutoFocus = true;
}
return this;
}
public Builder withFlash(Boolean flashAvailable) {
this.flashAvailable = flashAvailable;
return this;
}
public Builder withAeCompensationRange(Range<Integer> aeCompensationRange) {
this.aeCompensationRange = aeCompensationRange;
return this;
}
public Builder withAeCompensationStep(Rational rational) {
aeCompensationRatio = rational;
return this;
}
public CameraCharacteristicsModel build() {
return new CameraCharacteristicsModel(
this.maxZoom, this.availablePreviewZone, this.hasAutoFocus, this.flashAvailable, this.aeCompensationRange, this.aeCompensationRatio
);
}
}
}
package com.apparence.camerawesome.models;
public enum FlashMode {
NONE,
ON,
AUTO,
ALWAYS
}
package com.apparence.camerawesome.sensors;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import io.flutter.plugin.common.EventChannel;
import static android.content.Context.SENSOR_SERVICE;
public class BasicLuminosityNotifier implements LuminosityNotifier, EventChannel.StreamHandler {
SensorManager mSensorManager;
Sensor mLightSensor;
EventChannel.EventSink notifyChannel;
@Override
public void init(Context context) {
if(mSensorManager != null && mLightSensor != null)
return;
mSensorManager = (SensorManager) context.getSystemService(SENSOR_SERVICE);
mLightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
mSensorManager.registerListener(lightListener, mLightSensor, SensorManager.SENSOR_DELAY_UI);
}
final SensorEventListener lightListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
if(notifyChannel != null && event != null && event.values != null && event.values.length > 0) {
notifyChannel.success(event.values[0]);
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) { }
};
@Override
public void onListen(Object arguments, EventChannel.EventSink events) {
this.notifyChannel = events;
}
@Override
public void onCancel(Object arguments) {
this.notifyChannel.endOfStream();
this.notifyChannel = null;
}
}
package com.apparence.camerawesome.sensors;
public enum CameraSensor {
FRONT,
BACK
}
package com.apparence.camerawesome.sensors;
import android.content.Context;
public interface LuminosityNotifier {
void init(Context context);
}
package com.apparence.camerawesome.sensors
interface SensorOrientation {
fun onOrientationChanged(orientation: Int)
}
\ No newline at end of file
package com.apparence.camerawesome.sensors
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
class SensorOrientationListener : EventChannel.StreamHandler, SensorOrientation {
var events: EventSink? = null
override fun onListen(arguments: Any, events: EventSink) {
this.events = events
}
override fun onCancel(arguments: Any?) {
events?.endOfStream()
events = null
}
override fun onOrientationChanged(orientation: Int) {
if (events == null) {
return
}
when (orientation) {
0 -> events!!.success("PORTRAIT_UP")
90 -> events!!.success("LANDSCAPE_LEFT")
180 -> events!!.success("PORTRAIT_DOWN")
270 -> events!!.success("LANDSCAPE_RIGHT")
}
}
}
\ No newline at end of file
package com.apparence.camerawesome.surface;
import android.graphics.SurfaceTexture;
import android.os.Build;
import android.util.Size;
import android.view.Surface;
import androidx.annotation.RequiresApi;
import io.flutter.view.TextureRegistry;
public class FlutterSurfaceFactory implements SurfaceFactory {
private TextureRegistry registry;
private TextureRegistry.SurfaceTextureEntry flutterTexture;
public FlutterSurfaceFactory(TextureRegistry registry) {
this.registry = registry;
}
@Override
public Surface build(Size size) {
flutterTexture = registry.createSurfaceTexture();
SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture();
surfaceTexture.setDefaultBufferSize(size.getWidth(), size.getHeight());
return new Surface(surfaceTexture);
}
@Override
public long getSurfaceId() {
if(flutterTexture == null) {
throw new RuntimeException("flutterTexture is null");
}
return flutterTexture.id();
}
}
package com.apparence.camerawesome.surface;
import android.util.Size;
import android.view.Surface;
public interface SurfaceFactory {
/**
* Creates a surfaceTexture used to create a Surface
* Surface are used to show camera preview
* @param previewSize
* @return
*/
Surface build(Size previewSize);
long getSurfaceId();
}
package com.apparence.camerawesome.utils
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.AbstractQueuedSynchronizer
/**
* A synchronization aid that allows one or more threads to wait until
* a set of operations being performed in other threads completes.
*
*
* A `CountDownLatch` is initialized with a given *count*.
* The [await][.await] methods block until the current count reaches
* zero due to invocations of the [.countDown] method, after which
* all waiting threads are released and any subsequent invocations of
* [await][.await] return immediately. This is a one-shot phenomenon
* -- the count cannot be reset. If you need a version that resets the
* count, consider using a [CyclicBarrier].
*
*
* A `CountDownLatch` is a versatile synchronization tool
* and can be used for a number of purposes. A
* `CountDownLatch` initialized with a count of one serves as a
* simple on/off latch, or gate: all threads invoking [await][.await]
* wait at the gate until it is opened by a thread invoking [ ][.countDown]. A `CountDownLatch` initialized to *N*
* can be used to make one thread wait until *N* threads have
* completed some action, or some action has been completed N times.
*
*
* A useful property of a `CountDownLatch` is that it
* doesn't require that threads calling `countDown` wait for
* the count to reach zero before proceeding, it simply prevents any
* thread from proceeding past an [await][.await] until all
* threads could pass.
*
*
* **Sample usage:** Here is a pair of classes in which a group
* of worker threads use two countdown latches:
*
* * The first is a start signal that prevents any worker from proceeding
* until the driver is ready for them to proceed;
* * The second is a completion signal that allows the driver to wait
* until all workers have completed.
*
*
* <pre>
* class Driver { // ...
* void main() throws InterruptedException {
* CountDownLatch startSignal = new CountDownLatch(1);
* CountDownLatch doneSignal = new CountDownLatch(N);
*
* for (int i = 0; i < N; ++i) // create and start threads
* new Thread(new Worker(startSignal, doneSignal)).start();
*
* doSomethingElse(); // don't let run yet
* startSignal.countDown(); // let all threads proceed
* doSomethingElse();
* doneSignal.await(); // wait for all to finish
* }
* }
*
* class Worker implements Runnable {
* private final CountDownLatch startSignal;
* private final CountDownLatch doneSignal;
* Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
* this.startSignal = startSignal;
* this.doneSignal = doneSignal;
* }
* public void run() {
* try {
* startSignal.await();
* doWork();
* doneSignal.countDown();
* } catch (InterruptedException ex) {} // return;
* }
*
* void doWork() { ... }
* }
*
</pre> *
*
*
* Another typical usage would be to divide a problem into N parts,
* describe each part with a Runnable that executes that portion and
* counts down on the latch, and queue all the Runnables to an
* Executor. When all sub-parts are complete, the coordinating thread
* will be able to pass through await. (When threads must repeatedly
* count down in this way, instead use a [CyclicBarrier].)
*
* <pre>
* class Driver2 { // ...
* void main() throws InterruptedException {
* CountDownLatch doneSignal = new CountDownLatch(N);
* Executor e = ...
*
* for (int i = 0; i < N; ++i) // create and start threads
* e.execute(new WorkerRunnable(doneSignal, i));
*
* doneSignal.await(); // wait for all to finish
* }
* }
*
* class WorkerRunnable implements Runnable {
* private final CountDownLatch doneSignal;
* private final int i;
* WorkerRunnable(CountDownLatch doneSignal, int i) {
* this.doneSignal = doneSignal;
* this.i = i;
* }
* public void run() {
* try {
* doWork(i);
* doneSignal.countDown();
* } catch (InterruptedException ex) {} // return;
* }
*
* void doWork() { ... }
* }
*
</pre> *
*
*
* Memory consistency effects: Actions in a thread prior to calling
* `countDown()`
* [*happen-before*](package-summary.html#MemoryVisibility)
* actions following a successful return from a corresponding
* `await()` in another thread.
*
* @since 1.5
* @author Doug Lea
*/
class ResettableCountDownLatch(count: Int) {
/**
* Synchronization control For CountDownLatch.
* Uses AQS state to represent count.
*/
private class Sync internal constructor(val startCount: Int) : AbstractQueuedSynchronizer() {
init {
state = startCount
}
val count: Int
get() = state
public override fun tryAcquireShared(acquires: Int): Int {
return if (state == 0) 1 else -1
}
public override fun tryReleaseShared(releases: Int): Boolean {
// Decrement count; signal when transition to zero
while (true) {
val c = state
if (c == 0) return false
val nextc = c - 1
if (compareAndSetState(c, nextc)) return nextc == 0
}
}
fun reset() {
state = startCount
}
companion object {
private const val serialVersionUID = 4982264981922014374L
}
}
private val sync: Sync
/**
* Constructs a `CountDownLatch` initialized with the given count.
*
* @param count the number of times [.countDown] must be invoked
* before threads can pass through [.await]
* @throws IllegalArgumentException if `count` is negative
*/
init {
require(count >= 0) { "count < 0" }
sync = Sync(count)
}
/**
* Causes the current thread to wait until the latch has counted down to
* zero, unless the thread is [interrupted][Thread.interrupt].
*
*
* If the current count is zero then this method returns immediately.
*
*
* If the current count is greater than zero then the current
* thread becomes disabled for thread scheduling purposes and lies
* dormant until one of two things happen:
*
* * The count reaches zero due to invocations of the
* [.countDown] method; or
* * Some other thread [interrupts][Thread.interrupt]
* the current thread.
*
*
*
* If the current thread:
*
* * has its interrupted status set on entry to this method; or
* * is [interrupted][Thread.interrupt] while waiting,
*
* then [InterruptedException] is thrown and the current thread's
* interrupted status is cleared.
*
* @throws InterruptedException if the current thread is interrupted
* while waiting
*/
@Throws(InterruptedException::class)
fun await() {
sync.acquireSharedInterruptibly(1)
}
fun reset() {
sync.reset()
}
/**
* Causes the current thread to wait until the latch has counted down to
* zero, unless the thread is [interrupted][Thread.interrupt],
* or the specified waiting time elapses.
*
*
* If the current count is zero then this method returns immediately
* with the value `true`.
*
*
* If the current count is greater than zero then the current
* thread becomes disabled for thread scheduling purposes and lies
* dormant until one of three things happen:
*
* * The count reaches zero due to invocations of the
* [.countDown] method; or
* * Some other thread [interrupts][Thread.interrupt]
* the current thread; or
* * The specified waiting time elapses.
*
*
*
* If the count reaches zero then the method returns with the
* value `true`.
*
*
* If the current thread:
*
* * has its interrupted status set on entry to this method; or
* * is [interrupted][Thread.interrupt] while waiting,
*
* then [InterruptedException] is thrown and the current thread's
* interrupted status is cleared.
*
*
* If the specified waiting time elapses then the value `false`
* is returned. If the time is less than or equal to zero, the method
* will not wait at all.
*
* @param timeout the maximum time to wait
* @param unit the time unit of the `timeout` argument
* @return `true` if the count reached zero and `false`
* if the waiting time elapsed before the count reached zero
* @throws InterruptedException if the current thread is interrupted
* while waiting
*/
@Throws(InterruptedException::class)
fun await(timeout: Long, unit: TimeUnit): Boolean {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout))
}
/**
* Decrements the count of the latch, releasing all waiting threads if
* the count reaches zero.
*
*
* If the current count is greater than zero then it is decremented.
* If the new count is zero then all waiting threads are re-enabled for
* thread scheduling purposes.
*
*
* If the current count equals zero then nothing happens.
*/
fun countDown() {
sync.releaseShared(1)
}
/**
* Returns the current count.
*
*
* This method is typically used for debugging and testing purposes.
*
* @return the current count
*/
val count: Int
get() = sync.count
/**
* Returns a string identifying this latch, as well as its state.
* The state, in brackets, includes the String `"Count ="`
* followed by the current count.
*
* @return a string identifying this latch, as well as its state
*/
override fun toString(): String {
return super.toString() + "[Count = " + sync.count + "]"
}
}
\ No newline at end of file
package com.apparence.camerawesome_example
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/lib" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.idea" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/example/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/example/.pub" />
<excludeFolder url="file://$MODULE_DIR$/example/build" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Flutter Plugins" level="project" />
</component>
</module>
\ No newline at end of file
This diff is collapsed.
# Licence
Copyright © Apparence.io <br/>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:<br/>
<br/>
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.<br/>
The Software is provided “as is”, without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the Software.
\ No newline at end of file
## 🚀 Roadmap
### Both platforms
- [ ] Adjust exposure in awesome UI (flutter)
- [ ] Timer before taking a photo (flutter)
- [ ] Multiple camera photo mode
- [ ] Rework quality options (use preset quality & not resolution)
- [ ] Add Web support.
- [ ] Add Linux support.
- [ ] Add Windows support.
- [ ] Add macOS support.
- [ ] Add custom filter.
### Android
- [ ] Include cameraX extensions (https://github.com/android/camera-samples/tree/main/CameraXExtensions)
- [ ] Change sensor type
- [ ] Add video settings
- [ ] Return list of all available cameras
### iOS
- [ ] Fix patrol tests.
- [x] Add correction brightness.
## ✔️ Done
- [x] Preview alignment & padding
- [x] Built-in widgets theming
- [x] Apply Preview filter
- [x] Apply filter on image
- [x] Add filters.
- [x] Use Pigeon.
- [x] Cropped ratio support
- [x] Tests E2E using `patrol`
- [x] Pause/Resume a video recording in awesome UI
- [x] Lock UI while recording a video (user should not change the camera sensor nor the camera mode)
- [x] Customize preview fit
- [x] Tap to focus on a point.
- [x] Add analysis mode.
- [x] Return list of all available cameras (iOS).
- [x] Change sensor type (iOS).
- [x] Add video settings (iOS).
- [x] Return list of all available cameras (iOS).
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This image diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment