try to handle changing video quality and image resolution

- CameraX video allowed predefined buckets of qualities UHD, FHD, HD, SD (defined in VideoQuality enum),
the CameraXPreview is configured to use the highest quality and
 CameraX will select the closest corresponding resolution supported by the device.
- tentatively add ChangeResolutionDialogX (which would be renamed back to ChangeResolutionDialog) to give user option to select photo resolution and video qualities
- add ImageQualityManager which performs the same operation for getting all resolutions supported by a device using the Camera2 API, as defined in the legacy CameraPreview
- add VideoQualityManager to manage saving/ getting user selected quality.
This commit is contained in:
darthpaul 2022-07-08 00:12:03 +01:00
parent 889a384f21
commit 74e2656831
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
}
}