fix: Fix 60 FPS crashing on some Samsungs (#2556)

* fix: Fix 60 FPS crash on Samsung by checking `CamcorderProfile.maxFps`

* Log FPS clamp

* Update CameraDeviceDetails.kt

* Format code
This commit is contained in:
Marc Rousavy 2024-02-14 12:47:03 +01:00 committed by GitHub
parent 3699ccde94
commit 478688529b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 120 additions and 69 deletions

View File

@ -1,12 +1,12 @@
package com.mrousavy.camera.core package com.mrousavy.camera.core
import android.annotation.SuppressLint
import android.graphics.ImageFormat import android.graphics.ImageFormat
import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraExtensionCharacteristics import android.hardware.camera2.CameraExtensionCharacteristics
import android.hardware.camera2.CameraManager import android.hardware.camera2.CameraManager
import android.hardware.camera2.CameraMetadata import android.hardware.camera2.CameraMetadata
import android.os.Build import android.os.Build
import android.util.Log
import android.util.Range import android.util.Range
import android.util.Size import android.util.Size
import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Arguments
@ -22,11 +22,15 @@ import com.mrousavy.camera.types.LensFacing
import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.Orientation
import com.mrousavy.camera.types.PixelFormat import com.mrousavy.camera.types.PixelFormat
import com.mrousavy.camera.types.VideoStabilizationMode import com.mrousavy.camera.types.VideoStabilizationMode
import com.mrousavy.camera.utils.CamcorderProfileUtils
import kotlin.math.atan2 import kotlin.math.atan2
import kotlin.math.sqrt import kotlin.math.sqrt
@SuppressLint("InlinedApi")
class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId: String) { class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId: String) {
companion object {
private const val TAG = "CameraDeviceDetails"
}
val characteristics by lazy { cameraManager.getCameraCharacteristics(cameraId) } val characteristics by lazy { cameraManager.getCameraCharacteristics(cameraId) }
val hardwareLevel by lazy { HardwareLevel.fromCameraCharacteristics(characteristics) } val hardwareLevel by lazy { HardwareLevel.fromCameraCharacteristics(characteristics) }
val capabilities by lazy { characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) ?: IntArray(0) } val capabilities by lazy { characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) ?: IntArray(0) }
@ -200,7 +204,15 @@ class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId
videoSizes.forEach { videoSize -> videoSizes.forEach { videoSize ->
val frameDuration = cameraConfig.getOutputMinFrameDuration(videoFormat, videoSize) val frameDuration = cameraConfig.getOutputMinFrameDuration(videoFormat, videoSize)
val maxFps = (1.0 / (frameDuration.toDouble() / 1_000_000_000)).toInt() var maxFps = (1.0 / (frameDuration.toDouble() / 1_000_000_000)).toInt()
val maxEncoderFps = CamcorderProfileUtils.getMaximumFps(cameraId, videoSize)
if (maxEncoderFps != null && maxEncoderFps < maxFps) {
Log.i(
TAG,
"Camera could do $maxFps FPS at $videoSize, but Media Encoder can only do $maxEncoderFps FPS. Clamping to $maxEncoderFps FPS..."
)
maxFps = maxEncoderFps
}
photoSizes.forEach { photoSize -> photoSizes.forEach { photoSize ->
val map = buildFormatMap(photoSize, videoSize, Range(1, maxFps)) val map = buildFormatMap(photoSize, videoSize, Range(1, maxFps))

View File

@ -1,39 +1,13 @@
package com.mrousavy.camera.extensions package com.mrousavy.camera.extensions
import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraCharacteristics
import android.media.CamcorderProfile
import android.os.Build
import android.util.Size import android.util.Size
import com.mrousavy.camera.utils.CamcorderProfileUtils
private fun getMaximumVideoSize(cameraId: String): Size? {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val profiles = CamcorderProfile.getAll(cameraId, CamcorderProfile.QUALITY_HIGH)
if (profiles != null) {
val largestProfile = profiles.videoProfiles.filterNotNull().maxByOrNull { it.width * it.height }
if (largestProfile != null) {
return Size(largestProfile.width, largestProfile.height)
}
}
}
val cameraIdInt = cameraId.toIntOrNull()
if (cameraIdInt != null) {
val profile = CamcorderProfile.get(cameraIdInt, CamcorderProfile.QUALITY_HIGH)
return Size(profile.videoFrameWidth, profile.videoFrameHeight)
}
return null
} catch (e: Throwable) {
// some Samsung phones just crash when trying to get the CamcorderProfile. Only god knows why.
return null
}
}
fun CameraCharacteristics.getVideoSizes(cameraId: String, format: Int): List<Size> { fun CameraCharacteristics.getVideoSizes(cameraId: String, format: Int): List<Size> {
val config = this.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! val config = this.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
val sizes = config.getOutputSizes(format) ?: emptyArray() val sizes = config.getOutputSizes(format) ?: emptyArray()
val maxVideoSize = getMaximumVideoSize(cameraId) val maxVideoSize = CamcorderProfileUtils.getMaximumVideoSize(cameraId)
if (maxVideoSize != null) { if (maxVideoSize != null) {
return sizes.filter { it.bigger <= maxVideoSize.bigger } return sizes.filter { it.bigger <= maxVideoSize.bigger }
} }

View File

@ -4,9 +4,9 @@ import android.media.CamcorderProfile
import android.media.MediaRecorder.VideoEncoder import android.media.MediaRecorder.VideoEncoder
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import android.util.Size
import com.mrousavy.camera.core.RecordingSession import com.mrousavy.camera.core.RecordingSession
import com.mrousavy.camera.types.VideoCodec import com.mrousavy.camera.types.VideoCodec
import com.mrousavy.camera.utils.CamcorderProfileUtils
import kotlin.math.abs import kotlin.math.abs
data class RecommendedProfile( data class RecommendedProfile(
@ -23,7 +23,7 @@ fun RecordingSession.getRecommendedBitRate(fps: Int, codec: VideoCodec, hdr: Boo
val targetResolution = size val targetResolution = size
val encoder = codec.toVideoEncoder() val encoder = codec.toVideoEncoder()
val bitDepth = if (hdr) 10 else 8 val bitDepth = if (hdr) 10 else 8
val quality = findClosestCamcorderProfileQuality(cameraId, targetResolution) val quality = CamcorderProfileUtils.findClosestCamcorderProfileQuality(cameraId, targetResolution, true)
Log.i("CamcorderProfile", "Closest matching CamcorderProfile: $quality") Log.i("CamcorderProfile", "Closest matching CamcorderProfile: $quality")
var recommendedProfile: RecommendedProfile? = null var recommendedProfile: RecommendedProfile? = null
@ -75,39 +75,3 @@ fun RecordingSession.getRecommendedBitRate(fps: Int, codec: VideoCodec, hdr: Boo
} }
return bitRate.toInt() return bitRate.toInt()
} }
private fun getResolutionForCamcorderProfileQuality(camcorderProfile: Int): Int =
when (camcorderProfile) {
CamcorderProfile.QUALITY_QCIF -> 176 * 144
CamcorderProfile.QUALITY_QVGA -> 320 * 240
CamcorderProfile.QUALITY_CIF -> 352 * 288
CamcorderProfile.QUALITY_VGA -> 640 * 480
CamcorderProfile.QUALITY_480P -> 720 * 480
CamcorderProfile.QUALITY_720P -> 1280 * 720
CamcorderProfile.QUALITY_1080P -> 1920 * 1080
CamcorderProfile.QUALITY_2K -> 2048 * 1080
CamcorderProfile.QUALITY_QHD -> 2560 * 1440
CamcorderProfile.QUALITY_2160P -> 3840 * 2160
CamcorderProfile.QUALITY_4KDCI -> 4096 * 2160
CamcorderProfile.QUALITY_8KUHD -> 7680 * 4320
else -> throw Error("Invalid CamcorderProfile \"$camcorderProfile\"!")
}
private fun findClosestCamcorderProfileQuality(cameraId: String, resolution: Size): Int {
// Iterate through all available CamcorderProfiles and find the one that matches the closest
val targetResolution = resolution.width * resolution.height
val cameraIdInt = cameraId.toIntOrNull()
val profiles = (CamcorderProfile.QUALITY_QCIF..CamcorderProfile.QUALITY_8KUHD).filter { profile ->
if (cameraIdInt != null) {
return@filter CamcorderProfile.hasProfile(cameraIdInt, profile)
} else {
return@filter CamcorderProfile.hasProfile(profile)
}
}
val closestProfile = profiles.minBy { profile ->
val currentResolution = getResolutionForCamcorderProfileQuality(profile)
return@minBy abs(currentResolution - targetResolution)
}
return closestProfile
}

View File

@ -0,0 +1,101 @@
package com.mrousavy.camera.utils
import android.media.CamcorderProfile
import android.os.Build
import android.util.Size
import kotlin.math.abs
class CamcorderProfileUtils {
companion object {
private fun getResolutionForCamcorderProfileQuality(camcorderProfile: Int): Int =
when (camcorderProfile) {
CamcorderProfile.QUALITY_QCIF -> 176 * 144
CamcorderProfile.QUALITY_QVGA -> 320 * 240
CamcorderProfile.QUALITY_CIF -> 352 * 288
CamcorderProfile.QUALITY_VGA -> 640 * 480
CamcorderProfile.QUALITY_480P -> 720 * 480
CamcorderProfile.QUALITY_720P -> 1280 * 720
CamcorderProfile.QUALITY_1080P -> 1920 * 1080
CamcorderProfile.QUALITY_2K -> 2048 * 1080
CamcorderProfile.QUALITY_QHD -> 2560 * 1440
CamcorderProfile.QUALITY_2160P -> 3840 * 2160
CamcorderProfile.QUALITY_4KDCI -> 4096 * 2160
CamcorderProfile.QUALITY_8KUHD -> 7680 * 4320
else -> throw Error("Invalid CamcorderProfile \"$camcorderProfile\"!")
}
fun findClosestCamcorderProfileQuality(cameraId: String, resolution: Size, allowLargerSize: Boolean): Int {
// Iterate through all available CamcorderProfiles and find the one that matches the closest
val targetResolution = resolution.width * resolution.height
val cameraIdInt = cameraId.toIntOrNull()
var profiles = (CamcorderProfile.QUALITY_QCIF..CamcorderProfile.QUALITY_8KUHD).filter { profile ->
if (cameraIdInt != null) {
return@filter CamcorderProfile.hasProfile(cameraIdInt, profile)
} else {
return@filter CamcorderProfile.hasProfile(profile)
}
}
if (!allowLargerSize) {
profiles = profiles.filter { profile ->
val currentResolution = getResolutionForCamcorderProfileQuality(profile)
return@filter currentResolution <= targetResolution
}
}
val closestProfile = profiles.minBy { profile ->
val currentResolution = getResolutionForCamcorderProfileQuality(profile)
return@minBy abs(currentResolution - targetResolution)
}
return closestProfile
}
fun getMaximumVideoSize(cameraId: String): Size? {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val profiles = CamcorderProfile.getAll(cameraId, CamcorderProfile.QUALITY_HIGH)
if (profiles != null) {
val largestProfile = profiles.videoProfiles.filterNotNull().maxByOrNull { it.width * it.height }
if (largestProfile != null) {
return Size(largestProfile.width, largestProfile.height)
}
}
}
val cameraIdInt = cameraId.toIntOrNull()
if (cameraIdInt != null) {
val profile = CamcorderProfile.get(cameraIdInt, CamcorderProfile.QUALITY_HIGH)
return Size(profile.videoFrameWidth, profile.videoFrameHeight)
}
return null
} catch (e: Throwable) {
// some Samsung phones just crash when trying to get the CamcorderProfile. Only god knows why.
return null
}
}
fun getMaximumFps(cameraId: String, size: Size): Int? {
try {
val quality = findClosestCamcorderProfileQuality(cameraId, size, false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val profiles = CamcorderProfile.getAll(cameraId, quality)
if (profiles != null) {
return profiles.videoProfiles.maxOf { profile -> profile.frameRate }
}
}
val cameraIdInt = cameraId.toIntOrNull()
if (cameraIdInt != null) {
val profile = CamcorderProfile.get(cameraIdInt, quality)
return profile.videoFrameRate
}
return null
} catch (e: Throwable) {
// some Samsung phones just crash when trying to get the CamcorderProfile. Only god knows why.
return null
}
}
}
}