diff --git a/app/src/main/java/app/grapheneos/camera/CamConfig.kt b/app/src/main/java/app/grapheneos/camera/CamConfig.kt index 128205f5..dd5670bf 100644 --- a/app/src/main/java/app/grapheneos/camera/CamConfig.kt +++ b/app/src/main/java/app/grapheneos/camera/CamConfig.kt @@ -7,6 +7,7 @@ import android.net.Uri import android.os.Build import android.provider.MediaStore import android.util.Log +import android.util.Range import android.util.Size import android.view.MotionEvent import android.view.View @@ -40,6 +41,7 @@ import androidx.core.content.ContextCompat import app.grapheneos.camera.analyzer.QRAnalyzer import app.grapheneos.camera.ktx.markAs16by9Layout import app.grapheneos.camera.ktx.markAs4by3Layout +import app.grapheneos.camera.ui.SettingsDialog import app.grapheneos.camera.ui.activities.CaptureActivity import app.grapheneos.camera.ui.activities.MainActivity import app.grapheneos.camera.ui.activities.MoreSettings @@ -86,6 +88,7 @@ class CamConfig(private val mActivity: MainActivity) { const val EMPHASIS_ON_QUALITY = "emphasis_on_quality" const val FOCUS_TIMEOUT = "focus_timeout" const val VIDEO_QUALITY = "video_quality" + const val VIDEO_FRAME_RATE = "video_frame_rate" const val ASPECT_RATIO = "aspect_ratio" const val INCLUDE_AUDIO = "include_audio" const val ENABLE_EIS = "enable_eis" @@ -124,6 +127,8 @@ class CamConfig(private val mActivity: MainActivity) { val VIDEO_QUALITY = Quality.HIGHEST + val VIDEO_FRAME_RATE = Range(30, 30) + const val SELF_ILLUMINATION = false const val GEO_TAGGING = false @@ -328,7 +333,7 @@ class CamConfig(private val mActivity: MainActivity) { var videoQuality: Quality = SettingValues.Default.VIDEO_QUALITY get() { return if (modePref.contains(videoQualityKey)) { - mActivity.settingsDialog.titleToQuality( + SettingsDialog.titleToQuality( modePref.getString(videoQualityKey, "")!! ) } else { @@ -357,6 +362,43 @@ class CamConfig(private val mActivity: MainActivity) { return "${SettingValues.Key.VIDEO_QUALITY}_$pf" } + var videoFrameRate: Range = SettingValues.Default.VIDEO_FRAME_RATE + get() { + return if (modePref.contains(videoFrameRateKey)) { + SettingsDialog.titleToFrameRateRange( + modePref.getString(videoFrameRateKey, "")!! + ) + } else { + defaultVideoFrameRate + } + } + set(value) { + modePref.edit { + putString(videoFrameRateKey, mActivity.settingsDialog.getTitleForFrameRateRange(value)) + } + + field = value + } + + val videoFrameRateKey : String + get() { + val pf = if (lensFacing == CameraSelector.LENS_FACING_FRONT) { + "FRONT" + } else { + "BACK" + } + + return "${SettingValues.Key.VIDEO_FRAME_RATE}_$pf" + } + + val defaultVideoFrameRate : Range + get() { + val availableFrameRates = getAvailableVideoFrameRates() + if (availableFrameRates.contains(SettingValues.Default.VIDEO_FRAME_RATE)) + return SettingValues.Default.VIDEO_FRAME_RATE + return availableFrameRates[0] + } + var flashMode: Int get() = if (imageCapture != null) imageCapture!!.flashMode else SettingValues.Default.FLASH_MODE @@ -711,7 +753,7 @@ class CamConfig(private val mActivity: MainActivity) { } if (isVideoMode) { - mActivity.settingsDialog.reloadQualities() + mActivity.settingsDialog.reloadVideoSettings() } if (lensFacing == CameraSelector.LENS_FACING_FRONT) { @@ -751,7 +793,7 @@ class CamConfig(private val mActivity: MainActivity) { editor.putBoolean(SettingValues.Key.CAMERA_SOUNDS, SettingValues.Default.CAMERA_SOUNDS) } - // Note: This is a workaround to keep save image/video as previewed 'on' by + // Note: This is a workaround to keep save image/video as previewed 'on' by // default starting from v73 and 'off' by default for versions before that // // If its not a fresh install (before v73) @@ -957,6 +999,19 @@ class CamConfig(private val mActivity: MainActivity) { return cameraProvider!!.getCameraInfo(cameraSelector) } + fun getAvailableVideoFrameRates(): List> { + val resSet = getCurrentCameraInfo().supportedFrameRateRanges + + // Individual fps -> Ranged fps (sorted by lower value of range and then upper for each lower value) + val resList = resSet.sortedWith(compareBy> { it.lower != it.upper }.thenBy { it.lower }.thenBy { it.upper }) + + // If the supportedFrameRateRange list is somehow empty due to device/library implementation + // go with the most likely default rate + if (resList.isEmpty()) return listOf(SettingValues.Default.VIDEO_FRAME_RATE) + + return resList + } + fun toggleCameraSelector() { // Manually switch to the opposite lens facing @@ -1157,6 +1212,8 @@ class CamConfig(private val mActivity: MainActivity) { if (mActivity.camConfig.saveVideoAsPreviewed) videoCaptureBuilder.setMirrorMode(MirrorMode.MIRROR_MODE_ON_FRONT_ONLY) + videoCaptureBuilder.setTargetFrameRate(mActivity.camConfig.videoFrameRate) + videoCapture = videoCaptureBuilder.build() useCaseGroupBuilder.addUseCase(videoCapture!!) diff --git a/app/src/main/java/app/grapheneos/camera/capturer/VideoCapturer.kt b/app/src/main/java/app/grapheneos/camera/capturer/VideoCapturer.kt index 9a7443fb..706f3c01 100644 --- a/app/src/main/java/app/grapheneos/camera/capturer/VideoCapturer.kt +++ b/app/src/main/java/app/grapheneos/camera/capturer/VideoCapturer.kt @@ -284,6 +284,7 @@ class VideoCapturer(private val mActivity: MainActivity) { animator.start() mActivity.settingsDialog.videoQualitySpinner.isEnabled = false + mActivity.settingsDialog.videoFrameRateSpinner.isEnabled = false mActivity.settingsDialog.enableEISToggle.isEnabled = false mActivity.flipCamIcon.setImageResource(R.drawable.pause) @@ -331,6 +332,7 @@ class VideoCapturer(private val mActivity: MainActivity) { mActivity.flipCamIcon.setImageResource(R.drawable.flip_camera) mActivity.settingsDialog.videoQualitySpinner.isEnabled = true + mActivity.settingsDialog.videoFrameRateSpinner.isEnabled = true mActivity.settingsDialog.enableEISToggle.isEnabled = true if (mActivity !is VideoCaptureActivity) { diff --git a/app/src/main/java/app/grapheneos/camera/ui/SettingsDialog.kt b/app/src/main/java/app/grapheneos/camera/ui/SettingsDialog.kt index 7a09d5c1..245cc48d 100644 --- a/app/src/main/java/app/grapheneos/camera/ui/SettingsDialog.kt +++ b/app/src/main/java/app/grapheneos/camera/ui/SettingsDialog.kt @@ -12,6 +12,7 @@ import android.os.Handler import android.os.Looper import android.provider.Settings import android.util.Log +import android.util.Range import android.view.MotionEvent import android.view.View import android.view.ViewGroup @@ -61,8 +62,13 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) : private var aRToggle: ToggleButton var torchToggle: ToggleButton private var gridToggle: ImageView + var videoQualitySpinner: Spinner - private lateinit var vQAdapter: ArrayAdapter + var videoFrameRateSpinner: Spinner + + private lateinit var videoQualityAdapter: ArrayAdapter + private lateinit var videoFrameRateAdapter: ArrayAdapter + private var focusTimeoutSpinner: Spinner private var timerSpinner: Spinner @@ -84,6 +90,7 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) : private var enableEISSetting: View private var selfIlluminationSetting: View private var videoQualitySetting: View + private var videoFrameRateSetting: LinearLayout private var timerSetting: View var settingsFrame: View @@ -226,19 +233,35 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) : videoQualitySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected( - p0: AdapterView<*>?, - p1: View?, + parent: AdapterView<*>?, + view: View?, position: Int, - p3: Long + id: Long ) { - - val choice = vQAdapter.getItem(position) as String + val choice = videoQualityAdapter.getItem(position) as String updateVideoQuality(choice) } - override fun onNothingSelected(p0: AdapterView<*>?) {} + override fun onNothingSelected(parent: AdapterView<*>?) {} } + videoFrameRateSpinner = binding.videoFrameRateSpinner + + videoFrameRateSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + val choice = videoFrameRateAdapter.getItem(position) as String + updateVideoFrameRate(choice) + } + + override fun onNothingSelected(parent: AdapterView<*>?) {} + + } + qRadio = binding.qualityRadio lRadio = binding.latencyRadio @@ -333,6 +356,7 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) : enableEISSetting = binding.enableEisSetting selfIlluminationSetting = binding.selfIlluminationSetting videoQualitySetting = binding.videoQualitySetting + videoFrameRateSetting = binding.videoFrameRateSetting timerSetting = binding.timerSetting includeAudioToggle = binding.includeAudioSwitch @@ -447,17 +471,18 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) : @androidx.camera.camera2.interop.ExperimentalCamera2Interop if (camConfig.isVideoMode) { includeAudioSetting.visibility = View.VISIBLE - enableEISSetting.visibility = View.GONE videoQualitySetting.visibility = View.VISIBLE enableEISSetting.visibility = if (camConfig.isVideoStabilizationSupported()) { View.VISIBLE } else { View.GONE } + videoFrameRateSetting.visibility = View.VISIBLE } else { includeAudioSetting.visibility = View.GONE enableEISSetting.visibility = View.GONE videoQualitySetting.visibility = View.GONE + videoFrameRateSetting.visibility = View.GONE } selfIlluminationSetting.visibility = @@ -510,21 +535,23 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) : if (resCam) { camConfig.startCamera(true) } else { - videoQualitySpinner.setSelection(getAvailableQTitles().indexOf(choice)) + videoQualitySpinner.setSelection(getAvailableQualityTitles().indexOf(choice)) } } - fun titleToQuality(title: String): Quality { - return when (title) { - "2160p (UHD)" -> Quality.UHD - "1080p (FHD)" -> Quality.FHD - "720p (HD)" -> Quality.HD - "480p (SD)" -> Quality.SD - else -> { - Log.e("TAG", "Unknown quality: $title") - Quality.SD - } + fun updateVideoFrameRate(choice: String, restartCamera: Boolean = true) { + + val videoFrameRate = titleToFrameRateRange(choice) + + if (videoFrameRate == camConfig.videoFrameRate) return + + camConfig.videoFrameRate = videoFrameRate + + if (restartCamera) { + camConfig.startCamera(true) + } else { + videoFrameRateSpinner.setSelection(videoFrameRateAdapter.getPosition(choice)) } } @@ -717,27 +744,25 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) : return Recorder.getVideoCapabilities(cameraInfo).getSupportedQualities(DynamicRange.SDR) } - private fun getAvailableQTitles(): List { + + private fun getAvailableQualityTitles(): List { val titles = arrayListOf() getAvailableQualities().forEach { - titles.add(getTitleFor(it)) + titles.add(getTitleForQuality(it)) } return titles } - private fun getTitleFor(quality: Quality): String { - return when (quality) { - Quality.UHD -> "2160p (UHD)" - Quality.FHD -> "1080p (FHD)" - Quality.HD -> "720p (HD)" - Quality.SD -> "480p (SD)" - else -> { - Log.i("TAG", "Unknown constant: $quality") - "Unknown" - } + private fun getAvailableFrameRateTitles(): List { + val titles = arrayListOf() + + camConfig.getAvailableVideoFrameRates().forEach { + titles.add(getTitleForFrameRateRange(it)) } + + return titles } fun updateGridToggleUI() { @@ -788,24 +813,91 @@ class SettingsDialog(val mActivity: MainActivity, themedContext: Context) : slideDialogDown() } - fun reloadQualities() { + fun reloadVideoSettings() { - val titles = getAvailableQTitles() + val qualityTitles = getAvailableQualityTitles() + val frameRateTitles = getAvailableFrameRateTitles() - vQAdapter = ArrayAdapter( + videoQualityAdapter = ArrayAdapter( mActivity, android.R.layout.simple_spinner_item, - titles + qualityTitles ) - vQAdapter.setDropDownViewResource( + videoQualityAdapter.setDropDownViewResource( android.R.layout.simple_spinner_dropdown_item ) - videoQualitySpinner.adapter = vQAdapter + videoQualitySpinner.adapter = videoQualityAdapter - if (camConfig.videoQuality != Quality.HIGHEST) { - videoQualitySpinner.setSelection(titles.indexOf(getTitleFor(camConfig.videoQuality))) + videoFrameRateAdapter = ArrayAdapter( + mActivity, + android.R.layout.simple_spinner_item, + frameRateTitles + ) + + videoFrameRateAdapter.setDropDownViewResource( + android.R.layout.simple_spinner_dropdown_item + ) + + videoFrameRateSpinner.adapter = videoFrameRateAdapter + + videoFrameRateSpinner.setSelection(videoFrameRateAdapter.getPosition(getTitleForFrameRateRange(camConfig.videoFrameRate))) + + if (camConfig.videoQuality != CamConfig.SettingValues.Default.VIDEO_QUALITY) { + videoQualitySpinner.setSelection(videoQualityAdapter.getPosition(getTitleForQuality(camConfig.videoQuality))) + } + } + + companion object { + fun titleToQuality(title: String): Quality { + return when (title) { + "2160p (UHD)" -> Quality.UHD + "1080p (FHD)" -> Quality.FHD + "720p (HD)" -> Quality.HD + "480p (SD)" -> Quality.SD + else -> { + Log.e("TAG", "Unknown quality: $title") + Quality.SD + } + } + } + + fun titleToFrameRateRange(title: String): Range { + val titleWithoutFps = title.dropLast(4) + + if (titleWithoutFps.contains("-")) { + val lUArr = titleWithoutFps.split("-") + + val lower = lUArr[0].dropLast(1).toInt() + val upper = lUArr[1].drop(1).toInt() + + return Range(lower, upper) + } else { + val fps = titleWithoutFps.toInt() + return Range(fps, fps) + } + } + } + + fun getTitleForFrameRateRange(range: Range) : String { + if (range.lower == range.upper) { + return "${range.lower} fps" + } else { + return "${range.lower} - ${range.upper} fps" + } + } + + private fun getTitleForQuality(quality: Quality): String { + return when (quality) { + Quality.UHD -> "2160p (UHD)" + Quality.FHD -> "1080p (FHD)" + Quality.HD -> "720p (HD)" + Quality.SD -> "480p (SD)" + else -> { + Log.i("TAG", "Unknown constant: $quality") + "Unknown" + } } } } diff --git a/app/src/main/res/layout/settings.xml b/app/src/main/res/layout/settings.xml index 2a0671f6..d6012ace 100644 --- a/app/src/main/res/layout/settings.xml +++ b/app/src/main/res/layout/settings.xml @@ -214,6 +214,39 @@ + + + + + + + + + + + The video\'s audio recording has been muted The video\'s audio recording has been unmuted + Video Frame Rate