Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fix 60 FPS crashing on some Samsungs #2556

Merged
merged 4 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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) }
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -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