Merge pull request #326 from KryptKode/feat/camera-x

try to handle changing video quality and image resolution
This commit is contained in:
Tibor Kaputa 2022-07-08 22:33:14 +02:00 committed by GitHub
commit 3c16ba8853
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 419 additions and 34 deletions

View File

@ -0,0 +1,104 @@
package com.simplemobiletools.camera.dialogs
import android.app.Activity
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.camera.R
import com.simplemobiletools.camera.extensions.config
import com.simplemobiletools.camera.models.MySize
import com.simplemobiletools.camera.models.VideoQuality
import com.simplemobiletools.commons.dialogs.RadioGroupDialog
import com.simplemobiletools.commons.extensions.setupDialogStuff
import com.simplemobiletools.commons.models.RadioItem
import kotlinx.android.synthetic.main.dialog_change_resolution.view.change_resolution_photo
import kotlinx.android.synthetic.main.dialog_change_resolution.view.change_resolution_photo_holder
import kotlinx.android.synthetic.main.dialog_change_resolution.view.change_resolution_video
import kotlinx.android.synthetic.main.dialog_change_resolution.view.change_resolution_video_holder
class ChangeResolutionDialogX(
private val activity: Activity,
private val isFrontCamera: Boolean,
private val photoResolutions: List<MySize> = listOf(),
private val videoResolutions: List<VideoQuality>,
private val callback: () -> Unit
) {
private var dialog: AlertDialog
private val config = activity.config
private val TAG = "ChangeResolutionDialogX"
init {
val view = LayoutInflater.from(activity).inflate(R.layout.dialog_change_resolution, null).apply {
setupPhotoResolutionPicker(this)
setupVideoResolutionPicker(this)
}
dialog = AlertDialog.Builder(activity)
.setPositiveButton(R.string.ok, null)
.create().apply {
activity.setupDialogStuff(view, this, if (isFrontCamera) R.string.front_camera else R.string.back_camera)
}
}
private fun setupPhotoResolutionPicker(view: View) {
val items = getFormattedResolutions(photoResolutions)
var selectionIndex = if (isFrontCamera) config.frontPhotoResIndex else config.backPhotoResIndex
selectionIndex = Math.max(selectionIndex, 0)
view.change_resolution_photo_holder.setOnClickListener {
RadioGroupDialog(activity, items, selectionIndex) {
selectionIndex = it as Int
Log.w(TAG, "setupPhotoResolutionPicker: selectionIndex=$it")
view.change_resolution_photo.text = items[selectionIndex].title
if (isFrontCamera) {
config.frontPhotoResIndex = it
} else {
config.backPhotoResIndex = it
}
dialog.dismiss()
callback.invoke()
}
}
view.change_resolution_photo.text = items.getOrNull(selectionIndex)?.title
}
private fun setupVideoResolutionPicker(view: View) {
val items = videoResolutions.mapIndexed { index, videoQuality ->
val megapixels = videoQuality.megaPixels
val aspectRatio = videoQuality.getAspectRatio(activity)
RadioItem(index, "${videoQuality.width} x ${videoQuality.height} ($megapixels MP, $aspectRatio)")
}
val videoQuality = if (isFrontCamera) config.frontVideoQuality else config.backVideoQuality
var selectionIndex = videoResolutions.indexOf(videoQuality)
view.change_resolution_video_holder.setOnClickListener {
RadioGroupDialog(activity, ArrayList(items), selectionIndex) {
selectionIndex = it as Int
val selectedItem = items[selectionIndex]
val selectedQuality = videoResolutions[selectionIndex]
view.change_resolution_video.text = selectedItem.title
if (isFrontCamera) {
config.frontVideoQuality = selectedQuality
} else {
config.backVideoQuality = selectedQuality
}
dialog.dismiss()
callback.invoke()
}
}
view.change_resolution_video.text = items.getOrNull(selectionIndex)?.title
}
private fun getFormattedResolutions(resolutions: List<MySize>): ArrayList<RadioItem> {
val items = ArrayList<RadioItem>(resolutions.size)
val sorted = resolutions.sortedByDescending { it.width * it.height }
sorted.forEachIndexed { index, size ->
val megapixels = String.format("%.1f", (size.width * size.height.toFloat()) / 1000000)
val aspectRatio = size.getAspectRatio(activity)
items.add(RadioItem(index, "${size.width} x ${size.height} ($megapixels MP, $aspectRatio)"))
}
return items
}
}

View File

@ -0,0 +1,32 @@
package com.simplemobiletools.camera.extensions
import androidx.camera.core.AspectRatio
import androidx.camera.video.Quality
import com.simplemobiletools.camera.models.VideoQuality
fun Quality.toVideoQuality(): VideoQuality {
return when (this) {
Quality.UHD -> VideoQuality.UHD
Quality.FHD -> VideoQuality.FHD
Quality.HD -> VideoQuality.HD
Quality.SD -> VideoQuality.SD
else -> throw IllegalArgumentException("Unsupported quality: $this")
}
}
fun VideoQuality.toCameraXQuality(): Quality {
return when (this) {
VideoQuality.UHD -> Quality.UHD
VideoQuality.FHD -> Quality.FHD
VideoQuality.HD -> Quality.HD
VideoQuality.SD -> Quality.SD
}
}
fun Quality.getAspectRatio(): Int {
return when(this) {
Quality.UHD, Quality.FHD, Quality.HD -> AspectRatio.RATIO_16_9
Quality.SD -> AspectRatio.RATIO_4_3
else -> throw IllegalArgumentException("Unsupported quality: $this")
}
}

View File

@ -3,6 +3,7 @@ package com.simplemobiletools.camera.helpers
import android.content.Context
import android.os.Environment
import androidx.camera.core.CameraSelector
import com.simplemobiletools.camera.models.VideoQuality
import com.simplemobiletools.commons.helpers.BaseConfig
import java.io.File
@ -62,6 +63,20 @@ class Config(context: Context) : BaseConfig(context) {
get() = prefs.getInt(FRONT_PHOTO_RESOLUTION_INDEX, 0)
set(frontPhotoResIndex) = prefs.edit().putInt(FRONT_PHOTO_RESOLUTION_INDEX, frontPhotoResIndex).apply()
var backVideoQuality: VideoQuality
get() {
val backQuality = prefs.getString(BACK_VIDEO_QUALITY, VideoQuality.UHD.name)
return VideoQuality.values().first { it.name == backQuality }
}
set(backVideoQuality) = prefs.edit().putString(BACK_VIDEO_QUALITY, backVideoQuality.name).apply()
var frontVideoQuality: VideoQuality
get() {
val frontQuality = prefs.getString(FRONT_VIDEO_QUALITY, VideoQuality.UHD.name)
return VideoQuality.values().first { it.name == frontQuality }
}
set(frontVideoQuality) = prefs.edit().putString(FRONT_VIDEO_QUALITY, frontVideoQuality.name).apply()
var frontVideoResIndex: Int
get() = prefs.getInt(FRONT_VIDEO_RESOLUTION_INDEX, 0)
set(frontVideoResIndex) = prefs.edit().putInt(FRONT_VIDEO_RESOLUTION_INDEX, frontVideoResIndex).apply()

View File

@ -15,6 +15,8 @@ const val FLASHLIGHT_STATE = "flashlight_state"
const val INIT_PHOTO_MODE = "init_photo_mode"
const val BACK_PHOTO_RESOLUTION_INDEX = "back_photo_resolution_index_2"
const val BACK_VIDEO_RESOLUTION_INDEX = "back_video_resolution_index_2"
const val BACK_VIDEO_QUALITY = "back_video_quality_2"
const val FRONT_VIDEO_QUALITY = "front_video_quality_2"
const val FRONT_PHOTO_RESOLUTION_INDEX = "front_photo_resolution_index_2"
const val FRONT_VIDEO_RESOLUTION_INDEX = "front_video_resolution_index_2"
const val KEEP_SETTINGS_VISIBLE = "keep_settings_visible"

View File

@ -0,0 +1,89 @@
package com.simplemobiletools.camera.helpers
import android.content.Context
import android.graphics.ImageFormat
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.hardware.camera2.params.StreamConfigurationMap
import android.media.MediaRecorder
import android.util.Log
import android.util.Size
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.video.Quality
import com.simplemobiletools.camera.extensions.config
import com.simplemobiletools.camera.extensions.toCameraXQuality
import com.simplemobiletools.camera.models.CameraSelectorImageQualities
import com.simplemobiletools.camera.models.CameraSelectorVideoQualities
import com.simplemobiletools.camera.models.MySize
class ImageQualityManager(
activity: AppCompatActivity,
) {
companion object {
private const val TAG = "ImageQualityManager"
private const val MAX_VIDEO_WIDTH = 4096
private const val MAX_VIDEO_HEIGHT = 2160
private val CAMERA_LENS = arrayOf(CameraCharacteristics.LENS_FACING_FRONT, CameraCharacteristics.LENS_FACING_BACK)
}
private val cameraManager = activity.getSystemService(Context.CAMERA_SERVICE) as CameraManager
private val config = activity.config
private val imageQualities = mutableListOf<CameraSelectorImageQualities>()
fun initSupportedQualities() {
for (cameraId in cameraManager.cameraIdList) {
try {
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
for (lens in CAMERA_LENS) {
if (characteristics.get(CameraCharacteristics.LENS_FACING) == lens) {
val configMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) ?: continue
val imageSizes = configMap.getOutputSizes(ImageFormat.JPEG).map { MySize(it.width, it.height) }
val cameraSelector = lens.toCameraSelector()
imageQualities.add(CameraSelectorImageQualities(cameraSelector, imageSizes))
Log.i(TAG, "initQualities: imageSizes=$imageSizes")
}
}
} catch (e: Exception) {
Log.e(TAG, "Camera ID=$cameraId is not supported", e)
}
}
}
private fun getAvailableVideoSizes(configMap: StreamConfigurationMap): List<Size> {
return configMap.getOutputSizes(MediaRecorder::class.java).filter {
it.width <= MAX_VIDEO_WIDTH && it.height <= MAX_VIDEO_HEIGHT
}
}
private fun Int.toCameraSelector(): CameraSelector {
return if (this == CameraCharacteristics.LENS_FACING_FRONT) {
CameraSelector.DEFAULT_FRONT_CAMERA
} else {
CameraSelector.DEFAULT_BACK_CAMERA
}
}
fun getUserSelectedResolution(cameraSelector: CameraSelector): Size? {
val index = if (cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA) config.frontPhotoResIndex else config.backPhotoResIndex
return imageQualities.filter { it.camSelector == cameraSelector }
.flatMap { it.qualities }
.sortedByDescending { it.pixels}
.distinctBy { it.pixels }
.map { Size(it.width, it.height) }
.also {
Log.i(TAG, "Resolutions: $it, index=$index")
}
.getOrNull(index).also {
Log.i(TAG, "getUserSelectedResolution: $it, index=$index")
}
}
fun getSupportedResolutions(cameraSelector: CameraSelector): List<MySize> {
return imageQualities.filter { it.camSelector == cameraSelector }
.flatMap { it.qualities }
.sortedByDescending { it.pixels }
.distinctBy { it.pixels }
}
}

View File

@ -0,0 +1,60 @@
package com.simplemobiletools.camera.helpers
import android.util.Log
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import com.simplemobiletools.camera.extensions.toCameraXQuality
import com.simplemobiletools.camera.extensions.toVideoQuality
import com.simplemobiletools.camera.models.CameraSelectorVideoQualities
import com.simplemobiletools.camera.models.VideoQuality
class VideoQualityManager(private val config: Config) {
companion object {
private const val TAG = "VideoQualityHelper"
private val QUALITIES = listOf(Quality.UHD, Quality.FHD, Quality.HD, Quality.SD)
private val CAMERA_SELECTORS = arrayOf(CameraSelector.DEFAULT_BACK_CAMERA, CameraSelector.DEFAULT_FRONT_CAMERA)
}
private val videoQualities = mutableListOf<CameraSelectorVideoQualities>()
fun initSupportedQualities(
cameraProvider: ProcessCameraProvider,
camera: Camera,
) {
if (videoQualities.isEmpty()) {
for (camSelector in CAMERA_SELECTORS) {
try {
if (cameraProvider.hasCamera(camSelector)) {
QualitySelector.getSupportedQualities(camera.cameraInfo)
.filter(QUALITIES::contains)
.also { allQualities ->
val qualities = allQualities.map { it.toVideoQuality() }
videoQualities.add(CameraSelectorVideoQualities(camSelector, qualities))
}
Log.i(TAG, "bindCameraUseCases: videoQualities=$videoQualities")
}
} catch (e: Exception) {
Log.e(TAG, "Camera Face $camSelector is not supported", e)
}
}
}
}
fun getUserSelectedQuality(cameraSelector: CameraSelector): Quality {
return if (cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA) {
config.frontVideoQuality.toCameraXQuality()
} else {
config.backVideoQuality.toCameraXQuality()
}
}
fun getSupportedQualities(cameraSelector: CameraSelector): List<VideoQuality> {
return videoQualities.filter { it.camSelector == cameraSelector }
.flatMap { it.qualities }
.sortedByDescending { it.pixels }
}
}

View File

@ -1,13 +1,9 @@
package com.simplemobiletools.camera.implementations
import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Context
import android.hardware.SensorManager
import android.hardware.display.DisplayManager
import android.net.Uri
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import android.view.Display
import android.view.GestureDetector
@ -36,14 +32,7 @@ import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.core.UseCase
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.FileDescriptorOutputOptions
import androidx.camera.video.MediaStoreOutputOptions
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.camera.video.VideoRecordEvent
import androidx.camera.video.*
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.core.view.doOnLayout
@ -52,23 +41,12 @@ import androidx.lifecycle.LifecycleOwner
import androidx.window.layout.WindowMetricsCalculator
import com.bumptech.glide.load.ImageHeaderParser.UNKNOWN_ORIENTATION
import com.simplemobiletools.camera.R
import com.simplemobiletools.camera.extensions.config
import com.simplemobiletools.camera.extensions.getRandomMediaName
import com.simplemobiletools.camera.extensions.toAppFlashMode
import com.simplemobiletools.camera.extensions.toCameraSelector
import com.simplemobiletools.camera.extensions.toLensFacing
import com.simplemobiletools.camera.helpers.CameraErrorHandler
import com.simplemobiletools.camera.helpers.MediaOutputHelper
import com.simplemobiletools.camera.helpers.MediaSoundHelper
import com.simplemobiletools.camera.helpers.PinchToZoomOnScaleGestureListener
import com.simplemobiletools.camera.dialogs.ChangeResolutionDialogX
import com.simplemobiletools.camera.extensions.*
import com.simplemobiletools.camera.helpers.*
import com.simplemobiletools.camera.interfaces.MyPreview
import com.simplemobiletools.camera.models.MediaOutput
import com.simplemobiletools.commons.extensions.hasPermission
import com.simplemobiletools.commons.extensions.toast
import com.simplemobiletools.commons.helpers.PERMISSION_RECORD_AUDIO
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
@ -98,6 +76,8 @@ class CameraXPreview(
private val displayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
private val mediaSoundHelper = MediaSoundHelper()
private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate()
private val videoQualityManager = VideoQualityManager(config)
private val imageQualityManager = ImageQualityManager(activity)
private val orientationEventListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) {
@SuppressLint("RestrictedApi")
@ -146,6 +126,7 @@ class CameraXPreview(
private fun startCamera(switching: Boolean = false) {
Log.i(TAG, "startCamera: ")
imageQualityManager.initSupportedQualities()
val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
cameraProviderFuture.addListener({
try {
@ -163,7 +144,12 @@ class CameraXPreview(
private fun bindCameraUseCases() {
val cameraProvider = cameraProvider ?: throw IllegalStateException("Camera initialization failed.")
val metrics = windowMetricsCalculator.computeCurrentWindowMetrics(activity).bounds
val aspectRatio = aspectRatio(metrics.width(), metrics.height())
val aspectRatio = if (isPhotoCapture) {
aspectRatio(metrics.width(), metrics.height())
} else {
val selectedQuality = videoQualityManager.getUserSelectedQuality(cameraSelector)
selectedQuality.getAspectRatio()
}
val rotation = previewView.display.rotation
preview = buildPreview(aspectRatio, rotation)
@ -174,7 +160,10 @@ class CameraXPreview(
cameraSelector,
preview,
captureUseCase,
)
).also {
videoQualityManager.initSupportedQualities(cameraProvider, it)
}
preview?.setSurfaceProvider(previewView.surfaceProvider)
setupZoomAndFocus()
}
@ -220,22 +209,30 @@ class CameraXPreview(
.setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY)
.setFlashMode(flashMode)
.setJpegQuality(config.photoQuality)
.setTargetAspectRatio(aspectRatio)
.setTargetRotation(rotation)
.apply {
imageQualityManager.getUserSelectedResolution(cameraSelector)?.let { resolution ->
Log.i(TAG, "buildImageCapture: resolution=$resolution")
setTargetResolution(resolution)
} ?: setTargetAspectRatio(aspectRatio)
}
.build()
}
private fun buildPreview(aspectRatio: Int, rotation: Int): Preview {
return Preview.Builder()
.setTargetAspectRatio(aspectRatio)
.setTargetRotation(rotation)
.setTargetAspectRatio(aspectRatio)
.build()
}
private fun buildVideoCapture(): VideoCapture<Recorder> {
val qualitySelector = QualitySelector.from(
videoQualityManager.getUserSelectedQuality(cameraSelector),
FallbackStrategy.lowerQualityOrHigherThan(Quality.SD),
)
val recorder = Recorder.Builder()
//TODO: user control for quality
.setQualitySelector(QualitySelector.from(Quality.FHD))
.setQualitySelector(qualitySelector)
.build()
return VideoCapture.withOutput(recorder)
}
@ -305,7 +302,18 @@ class CameraXPreview(
}
override fun showChangeResolutionDialog() {
val oldQuality = videoQualityManager.getUserSelectedQuality(cameraSelector)
ChangeResolutionDialogX(
activity,
isFrontCameraInUse(),
imageQualityManager.getSupportedResolutions(cameraSelector),
videoQualityManager.getSupportedQualities(cameraSelector)
) {
if (oldQuality != videoQualityManager.getUserSelectedQuality(cameraSelector)) {
currentRecording?.stop()
}
startCamera()
}
}
override fun toggleFrontBackCamera() {
@ -344,7 +352,6 @@ class CameraXPreview(
}
override fun tryTakePicture() {
Log.i(TAG, "captureImage: ")
val imageCapture = imageCapture ?: throw IllegalStateException("Camera initialization failed.")
val metadata = Metadata().apply {

View File

@ -0,0 +1,8 @@
package com.simplemobiletools.camera.models
import androidx.camera.core.CameraSelector
data class CameraSelectorImageQualities(
val camSelector: CameraSelector,
val qualities: List<MySize>,
)

View File

@ -0,0 +1,8 @@
package com.simplemobiletools.camera.models
import androidx.camera.core.CameraSelector
data class CameraSelectorVideoQualities(
val camSelector: CameraSelector,
val qualities: List<VideoQuality>,
)

View File

@ -6,6 +6,7 @@ import com.simplemobiletools.camera.R
data class MySize(val width: Int, val height: Int) {
val ratio = width / height.toFloat()
val pixels: Int = width * height
fun isSixteenToNine() = ratio == 16 / 9f
private fun isFiveToThree() = ratio == 5 / 3f

View File

@ -0,0 +1,59 @@
package com.simplemobiletools.camera.models
import android.content.Context
import com.simplemobiletools.camera.R
enum class VideoQuality(val width: Int, val height: Int) {
UHD(3840, 2160),
FHD(1920, 1080),
HD(1280, 720),
SD(720, 480);
val pixels: Int = width * height
val megaPixels: String = String.format("%.1f", (width * height.toFloat()) / VideoQuality.ONE_MEGA_PIXELS)
val ratio = width / height.toFloat()
private fun isSixteenToNine() = ratio == 16 / 9f
private fun isFiveToThree() = ratio == 5 / 3f
private fun isFourToThree() = ratio == 4 / 3f
private fun isTwoToOne() = ratio == 2f
private fun isThreeToFour() = ratio == 3 / 4f
private fun isThreeToTwo() = ratio == 3 / 2f
private fun isSixToFive() = ratio == 6 / 5f
private fun isNineteenToNine() = ratio == 19 / 9f
private fun isNineteenToEight() = ratio == 19 / 8f
private fun isOneNineToOne() = ratio == 1.9f
private fun isSquare() = width == height
fun getAspectRatio(context: Context) = when {
isSixteenToNine() -> "16:9"
isFiveToThree() -> "5:3"
isFourToThree() -> "4:3"
isThreeToFour() -> "3:4"
isThreeToTwo() -> "3:2"
isSixToFive() -> "6:5"
isOneNineToOne() -> "1.9:1"
isNineteenToNine() -> "19:9"
isNineteenToEight() -> "19:8"
isSquare() -> "1:1"
isTwoToOne() -> "2:1"
else -> context.resources.getString(R.string.other)
}
companion object {
private const val ONE_MEGA_PIXELS = 1000000
}
}