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:
parent
3699ccde94
commit
478688529b
@ -1,12 +1,12 @@
|
||||
package com.mrousavy.camera.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.ImageFormat
|
||||
import android.hardware.camera2.CameraCharacteristics
|
||||
import android.hardware.camera2.CameraExtensionCharacteristics
|
||||
import android.hardware.camera2.CameraManager
|
||||
import android.hardware.camera2.CameraMetadata
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.util.Range
|
||||
import android.util.Size
|
||||
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.PixelFormat
|
||||
import com.mrousavy.camera.types.VideoStabilizationMode
|
||||
import com.mrousavy.camera.utils.CamcorderProfileUtils
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
class CameraDeviceDetails(private val cameraManager: CameraManager, val cameraId: String) {
|
||||
companion object {
|
||||
private const val TAG = "CameraDeviceDetails"
|
||||
}
|
||||
|
||||
val characteristics by lazy { cameraManager.getCameraCharacteristics(cameraId) }
|
||||
val hardwareLevel by lazy { HardwareLevel.fromCameraCharacteristics(characteristics) }
|
||||
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 ->
|
||||
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 ->
|
||||
val map = buildFormatMap(photoSize, videoSize, Range(1, maxFps))
|
||||
|
@ -1,39 +1,13 @@
|
||||
package com.mrousavy.camera.extensions
|
||||
|
||||
import android.hardware.camera2.CameraCharacteristics
|
||||
import android.media.CamcorderProfile
|
||||
import android.os.Build
|
||||
import android.util.Size
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
import com.mrousavy.camera.utils.CamcorderProfileUtils
|
||||
|
||||
fun CameraCharacteristics.getVideoSizes(cameraId: String, format: Int): List<Size> {
|
||||
val config = this.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
|
||||
val sizes = config.getOutputSizes(format) ?: emptyArray()
|
||||
val maxVideoSize = getMaximumVideoSize(cameraId)
|
||||
val maxVideoSize = CamcorderProfileUtils.getMaximumVideoSize(cameraId)
|
||||
if (maxVideoSize != null) {
|
||||
return sizes.filter { it.bigger <= maxVideoSize.bigger }
|
||||
}
|
||||
|
@ -4,9 +4,9 @@ import android.media.CamcorderProfile
|
||||
import android.media.MediaRecorder.VideoEncoder
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.util.Size
|
||||
import com.mrousavy.camera.core.RecordingSession
|
||||
import com.mrousavy.camera.types.VideoCodec
|
||||
import com.mrousavy.camera.utils.CamcorderProfileUtils
|
||||
import kotlin.math.abs
|
||||
|
||||
data class RecommendedProfile(
|
||||
@ -23,7 +23,7 @@ fun RecordingSession.getRecommendedBitRate(fps: Int, codec: VideoCodec, hdr: Boo
|
||||
val targetResolution = size
|
||||
val encoder = codec.toVideoEncoder()
|
||||
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")
|
||||
|
||||
var recommendedProfile: RecommendedProfile? = null
|
||||
@ -75,39 +75,3 @@ fun RecordingSession.getRecommendedBitRate(fps: Int, codec: VideoCodec, hdr: Boo
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user