From 478688529b425c3125bd769955b6bd66242e9945 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 14 Feb 2024 12:47:03 +0100 Subject: [PATCH] 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 --- .../camera/core/CameraDeviceDetails.kt | 18 +++- .../CameraCharacteristics+getOutputSizes.kt | 30 +----- .../RecordingSession+getRecommendedBitRate.kt | 40 +------ .../camera/utils/CamcorderProfileUtils.kt | 101 ++++++++++++++++++ 4 files changed, 120 insertions(+), 69 deletions(-) create mode 100644 package/android/src/main/java/com/mrousavy/camera/utils/CamcorderProfileUtils.kt diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt index dfa94af..43a09db 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraDeviceDetails.kt @@ -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)) diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCharacteristics+getOutputSizes.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCharacteristics+getOutputSizes.kt index 168dabe..883e4c6 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCharacteristics+getOutputSizes.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCharacteristics+getOutputSizes.kt @@ -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 { 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 } } diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/RecordingSession+getRecommendedBitRate.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/RecordingSession+getRecommendedBitRate.kt index ed6cee2..d616a6e 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/RecordingSession+getRecommendedBitRate.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/RecordingSession+getRecommendedBitRate.kt @@ -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 -} diff --git a/package/android/src/main/java/com/mrousavy/camera/utils/CamcorderProfileUtils.kt b/package/android/src/main/java/com/mrousavy/camera/utils/CamcorderProfileUtils.kt new file mode 100644 index 0000000..f1dc64e --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/utils/CamcorderProfileUtils.kt @@ -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 + } + } + } +}