fix: Clean up codebase

This commit is contained in:
Marc Rousavy 2023-08-21 14:24:06 +02:00
parent 07ba0e1a41
commit e1b04088c6
17 changed files with 26 additions and 235 deletions

View File

@ -221,13 +221,14 @@ class CameraSession(private val context: Context,
suspend fun startRecording(enableAudio: Boolean,
codec: VideoCodec,
fileType: VideoFileType,
callback: (video: RecordingSession.Video) -> Unit) {
callback: (video: RecordingSession.Video) -> Unit,
onError: (error: RecorderError) -> Unit) {
mutex.withLock {
if (recording != null) throw RecordingInProgressError()
val outputs = outputs ?: throw CameraNotReadyError()
val videoOutput = outputs.videoOutput ?: throw VideoNotEnabledError()
val recording = RecordingSession(context, enableAudio, videoOutput.size, fps, codec, orientation, fileType, callback)
val recording = RecordingSession(context, enableAudio, videoOutput.size, fps, codec, orientation, fileType, callback, onError)
recording.start()
this.recording = recording
}

View File

@ -9,6 +9,7 @@ import com.mrousavy.camera.parsers.Torch
import com.mrousavy.camera.parsers.VideoCodec
import com.mrousavy.camera.parsers.VideoFileType
import com.mrousavy.camera.utils.RecordingSession
import com.mrousavy.camera.utils.makeErrorMap
import java.util.*
suspend fun CameraView.startRecording(options: ReadableMap, onRecordCallback: Callback) {
@ -39,7 +40,11 @@ suspend fun CameraView.startRecording(options: ReadableMap, onRecordCallback: Ca
map.putDouble("duration", video.durationMs.toDouble() / 1000.0)
onRecordCallback(map, null)
}
cameraSession.startRecording(audio == true, codec, fileType, callback)
val onError = { error: RecorderError ->
val errorMap = makeErrorMap(error.code, error.message)
onRecordCallback(null, errorMap)
}
cameraSession.startRecording(audio == true, codec, fileType, callback, onError)
}
@SuppressLint("RestrictedApi")

View File

@ -30,7 +30,6 @@ suspend fun CameraView.takePhoto(optionsMap: ReadableMap): WritableMap {
val flash = options["flash"] as? String ?: "off"
val enableAutoRedEyeReduction = options["enableAutoRedEyeReduction"] == true
val enableAutoStabilization = options["enableAutoStabilization"] == true
val skipMetadata = options["skipMetadata"] == true
val flashMode = Flash.fromUnionValue(flash)
val qualityPrioritizationMode = QualityPrioritization.fromUnionValue(qualityPrioritization)
@ -58,8 +57,6 @@ suspend fun CameraView.takePhoto(optionsMap: ReadableMap): WritableMap {
map.putBoolean("isRawPhoto", photo.format == ImageFormat.RAW_SENSOR)
map.putBoolean("isMirrored", photo.isMirrored)
// TODO: Add metadata prop to resulting photo
return map
}
}

View File

@ -1,6 +1,5 @@
package com.mrousavy.camera
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.ThemedReactContext
@ -13,7 +12,7 @@ import com.mrousavy.camera.parsers.Torch
import com.mrousavy.camera.parsers.VideoStabilizationMode
@Suppress("unused")
class CameraViewManager(reactContext: ReactApplicationContext) : ViewGroupManager<CameraView>() {
class CameraViewManager() : ViewGroupManager<CameraView>() {
public override fun createViewInstance(context: ThemedReactContext): CameraView {
return CameraView(context)

View File

@ -40,17 +40,11 @@ class NoCameraDeviceError : CameraError("device", "no-device", "No device was se
class NoFlashAvailableError : CameraError("device", "flash-unavailable", "The Camera Device does not have a flash unit! Make sure you select a device where `hasFlash`/`hasTorch` is true!")
class PixelFormatNotSupportedError(format: String) : CameraError("device", "pixel-format-not-supported", "The pixelFormat $format is not supported on the given Camera Device!")
class FpsNotContainedInFormatError(fps: Int) : CameraError("format", "invalid-fps", "The given format cannot run at $fps FPS! Make sure your FPS is lower than `format.maxFps` but higher than `format.minFps`.")
class HdrNotContainedInFormatError : CameraError(
"format", "invalid-hdr",
"The currently selected format does not support HDR capture! " +
"Make sure you select a format which includes `supportsPhotoHDR`!"
)
class LowLightBoostNotContainedInFormatError : CameraError(
"format", "invalid-low-light-boost",
"The currently selected format does not support low-light boost (night mode)! " +
"Make sure you select a format which includes `supportsLowLightBoost`."
)
class CameraNotReadyError : CameraError("session", "camera-not-ready", "The Camera is not ready yet! Wait for the onInitialized() callback!")
class CameraCannotBeOpenedError(cameraId: String, error: CameraDeviceError) : CameraError("session", "camera-cannot-be-opened", "The given Camera device (id: $cameraId) could not be opened! Error: $error")
@ -62,48 +56,7 @@ class PhotoNotEnabledError : CameraError("capture", "photo-not-enabled", "Photo
class CaptureAbortedError(wasImageCaptured: Boolean) : CameraError("capture", "aborted", "The image capture was aborted! Was Image captured: $wasImageCaptured")
class UnknownCaptureError(wasImageCaptured: Boolean) : CameraError("capture", "unknown", "An unknown error occurred while trying to capture an Image! Was Image captured: $wasImageCaptured")
class VideoEncoderError(cause: Throwable?) : CameraError("capture", "encoder-error", "The recording failed while encoding.\n" +
"This error may be generated when the video or audio codec encounters an error during encoding. " +
"When this happens and the output file is generated, the output file is not properly constructed. " +
"The application will need to clean up the output file, such as deleting the file.",
cause)
class InvalidVideoOutputOptionsError(cause: Throwable?) : CameraError("capture", "invalid-video-options",
"The recording failed due to invalid output options.\n" +
"This error is generated when invalid output options have been used while preparing a recording",
cause)
class RecorderError(cause: Throwable?) : CameraError("capture", "recorder-error",
"The recording failed because the Recorder is in an unrecoverable error state.\n" +
"When this happens and the output file is generated, the output file is not properly constructed. " +
"The application will need to clean up the output file, such as deleting the file. " +
"Such an error will usually require creating a new Recorder object to start a new recording.",
cause)
class NoValidDataError(cause: Throwable?) : CameraError("capture", "no-valid-data",
"The recording failed because no valid data was produced to be recorded.\n" +
"This error is generated when the essential data for a recording to be played correctly is missing, for example, " +
"a recording must contain at least one key frame. The application will need to clean up the output file, such as deleting the file.",
cause)
class InactiveSourceError(cause: Throwable?) : CameraError("capture", "inactive-source",
"The recording failed because the source becomes inactive and stops sending frames.\n" +
"One case is that if camera is closed due to lifecycle stopped, the active recording will be finalized with this error, " +
"and the output will be generated, containing the frames produced before camera closing. " +
"Attempting to start a new recording will be finalized immediately if the source remains inactive and no output will be generated.",
cause)
class InsufficientStorageError(cause: Throwable?) : CameraError("capture", "insufficient-storage",
"The recording failed due to insufficient storage space.\n" +
"There are two possible cases that will cause this error.\n" +
"1. The storage is already full before the recording starts, so no output file will be generated.\n" +
"2. The storage becomes full during recording, so the output file will be generated.",
cause)
class FileSizeLimitReachedError(cause: Throwable?) : CameraError("capture", "file-size-limit-reached",
"The recording failed due to file size limitation.\n" +
"The file size limitation will refer to OutputOptions.getFileSizeLimit(). The output file will still be generated with this error.",
cause)
class RecorderError(name: String, extra: Int) : CameraError("capture", "recorder-error", "An error occured while recording a video! $name $extra")
class NoRecordingInProgressError : CameraError("capture", "no-recording-in-progress", "There was no active video recording in progress! Did you call stopRecording() twice?")
class RecordingInProgressError : CameraError("capture", "recording-in-progress", "There is already an active video recording in progress! Did you call startRecording() twice?")

View File

@ -1,27 +0,0 @@
package com.mrousavy.camera.extensions
import android.media.CamcorderProfile
import android.util.Size
private val qualitiesMap = mapOf(
Size(176 - 1, 144 - 1) to CamcorderProfile.QUALITY_LOW,
Size(176, 144) to CamcorderProfile.QUALITY_QCIF,
Size(320, 240) to CamcorderProfile.QUALITY_QVGA,
Size(352, 288) to CamcorderProfile.QUALITY_CIF,
Size(640, 480) to CamcorderProfile.QUALITY_VGA,
Size(720, 480) to CamcorderProfile.QUALITY_480P,
Size(1280, 720) to CamcorderProfile.QUALITY_720P,
Size(1920, 1080) to CamcorderProfile.QUALITY_1080P,
Size(2048, 1080) to CamcorderProfile.QUALITY_2K,
Size(2560, 1440) to CamcorderProfile.QUALITY_QHD,
Size(3840, 2160) to CamcorderProfile.QUALITY_2160P,
Size(4096, 2160) to CamcorderProfile.QUALITY_4KDCI,
Size(7680, 4320) to CamcorderProfile.QUALITY_8KUHD,
Size(7680 + 1, 4320 + 1) to CamcorderProfile.QUALITY_HIGH,
)
fun getCamcorderQualityForSize(size: Size): Int {
// Find closest match
val closestMatch = qualitiesMap.keys.closestTo(size)
return qualitiesMap[closestMatch] ?: CamcorderProfile.QUALITY_HIGH
}

View File

@ -1,36 +0,0 @@
package com.mrousavy.camera.extensions
import android.content.Context
import android.os.Build
import android.view.Surface
import android.view.WindowManager
import com.facebook.react.bridge.ReactContext
val Context.displayRotation: Int
get() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Context.display
this.display?.let { display ->
return display.rotation
}
// ReactContext.currentActivity.display
if (this is ReactContext) {
currentActivity?.display?.let { display ->
return display.rotation
}
}
}
// WindowManager.defaultDisplay
val windowManager = getSystemService(Context.WINDOW_SERVICE) as? WindowManager
if (windowManager != null) {
@Suppress("DEPRECATION") // deprecated since SDK 30
windowManager.defaultDisplay?.let { display ->
return display.rotation
}
}
// 0
return Surface.ROTATION_0
}

View File

@ -1,38 +0,0 @@
package com.mrousavy.camera.extensions
import android.hardware.camera2.params.DynamicRangeProfiles
import android.media.MediaCodecInfo
import android.media.MediaFormat
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
@RequiresApi(Build.VERSION_CODES.N)
private fun getTransferFunction(codecProfile: Int) = when (codecProfile) {
MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10 -> MediaFormat.COLOR_TRANSFER_HLG
MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10 -> MediaFormat.COLOR_TRANSFER_ST2084
MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10Plus -> MediaFormat.COLOR_TRANSFER_ST2084
else -> MediaFormat.COLOR_TRANSFER_SDR_VIDEO
}
fun MediaFormat.setDynamicRangeProfile(dynamicRangeProfile: Long) {
val profile = when (dynamicRangeProfile) {
DynamicRangeProfiles.HLG10 -> MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10
DynamicRangeProfiles.HDR10 -> MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10
DynamicRangeProfiles.HDR10_PLUS -> MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10Plus
else -> null
}
if (profile != null) {
Log.i("MediaFormat", "Using HDR Profile $profile")
this.setInteger(MediaFormat.KEY_PROFILE, profile)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
this.setInteger(MediaFormat.KEY_COLOR_STANDARD, MediaFormat.COLOR_STANDARD_BT2020)
this.setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_FULL)
this.setInteger(MediaFormat.KEY_COLOR_TRANSFER, getTransferFunction(profile))
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
this.setFeatureEnabled(MediaCodecInfo.CodecCapabilities.FEATURE_HdrEditing, true)
}
}
}

View File

@ -15,10 +15,6 @@ fun List<Size>.closestToOrMax(size: Size?): Size {
}
}
fun Collection<Size>.closestTo(size: Size): Size {
return this.minBy { abs(it.width - size.width) + abs(it.height - size.height) }
}
/**
* Rotate by a given Surface Rotation
*/

View File

@ -1,24 +0,0 @@
package com.mrousavy.camera.extensions
import com.facebook.react.bridge.WritableArray
fun WritableArray.pushInt(value: Int?) {
if (value == null)
this.pushNull()
else
this.pushInt(value)
}
fun WritableArray.pushDouble(value: Double?) {
if (value == null)
this.pushNull()
else
this.pushDouble(value)
}
fun WritableArray.pushBoolean(value: Boolean?) {
if (value == null)
this.pushNull()
else
this.pushBoolean(value)
}

View File

@ -15,10 +15,3 @@ fun WritableMap.putDouble(key: String, value: Double?) {
else
this.putDouble(key, value)
}
fun WritableMap.putBoolean(key: String, value: Boolean?) {
if (value == null)
this.putNull(key)
else
this.putBoolean(key, value)
}

View File

@ -11,13 +11,6 @@ enum class PixelFormat(override val unionValue: String): JSUnionValue {
NATIVE("native"),
UNKNOWN("unknown");
private fun bestMatch(formats: IntArray, targetFormats: Array<Int>): Int? {
targetFormats.forEach { format ->
if (formats.contains(format)) return format
}
return null
}
fun toImageFormat(): Int {
val result = when (this) {
YUV -> ImageFormat.YUV_420_888

View File

@ -1,20 +0,0 @@
package com.mrousavy.camera.utils
import androidx.exifinterface.media.ExifInterface
class ExifUtils {
companion object {
fun computeExifOrientation(rotationDegrees: Int, mirrored: Boolean) = when {
rotationDegrees == 0 && !mirrored -> ExifInterface.ORIENTATION_NORMAL
rotationDegrees == 0 && mirrored -> ExifInterface.ORIENTATION_FLIP_HORIZONTAL
rotationDegrees == 180 && !mirrored -> ExifInterface.ORIENTATION_ROTATE_180
rotationDegrees == 180 && mirrored -> ExifInterface.ORIENTATION_FLIP_VERTICAL
rotationDegrees == 270 && mirrored -> ExifInterface.ORIENTATION_TRANSVERSE
rotationDegrees == 90 && !mirrored -> ExifInterface.ORIENTATION_ROTATE_90
rotationDegrees == 90 && mirrored -> ExifInterface.ORIENTATION_TRANSPOSE
rotationDegrees == 270 && mirrored -> ExifInterface.ORIENTATION_ROTATE_270
rotationDegrees == 270 && !mirrored -> ExifInterface.ORIENTATION_TRANSVERSE
else -> ExifInterface.ORIENTATION_UNDEFINED
}
}
}

View File

@ -9,6 +9,7 @@ import android.os.Build
import android.util.Log
import android.util.Size
import android.view.Surface
import com.mrousavy.camera.RecorderError
import com.mrousavy.camera.parsers.Orientation
import com.mrousavy.camera.parsers.VideoCodec
import com.mrousavy.camera.parsers.VideoFileType
@ -22,7 +23,8 @@ class RecordingSession(context: Context,
private val codec: VideoCodec = VideoCodec.H264,
private val orientation: Orientation,
private val fileType: VideoFileType = VideoFileType.MP4,
private val callback: (video: Video) -> Unit) {
private val callback: (video: Video) -> Unit,
private val onError: (error: RecorderError) -> Unit) {
companion object {
private const val TAG = "RecordingSession"
// bits per second
@ -77,6 +79,12 @@ class RecordingSession(context: Context,
recorder.setOnErrorListener { _, what, extra ->
Log.e(TAG, "MediaRecorder Error: $what ($extra)")
stop()
val name = when (what) {
MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN -> "unknown"
MediaRecorder.MEDIA_ERROR_SERVER_DIED -> "server-died"
else -> "unknown"
}
onError(RecorderError(name, extra))
}
recorder.setOnInfoListener { _, what, extra ->
Log.i(TAG, "MediaRecorder Info: $what ($extra)")

View File

@ -2,10 +2,10 @@ package com.mrousavy.camera.utils.outputs
import android.graphics.ImageFormat
import android.hardware.HardwareBuffer
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.media.Image
import android.media.ImageReader
import android.os.Build
import android.util.Log
import android.util.Size
import android.view.Surface
@ -92,7 +92,6 @@ class CameraOutputs(val cameraId: String,
init {
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val config = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
Log.i(TAG, "Preparing Outputs for Camera $cameraId...")
@ -118,6 +117,8 @@ class CameraOutputs(val cameraId: String,
// Video output: High resolution repeating images (startRecording() or useFrameProcessor())
if (video != null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) throw Error("Video Recordings and/or Frame Processors are only available on API 29 and above!")
val size = characteristics.getVideoSizes(cameraId, video.format).closestToOrMax(video.targetSize)
val flags = HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE or HardwareBuffer.USAGE_VIDEO_ENCODE

View File

@ -64,7 +64,6 @@ const _CaptureButton: React.FC<Props> = ({
qualityPrioritization: 'speed',
flash: flash,
quality: 90,
skipMetadata: true,
}),
[flash],
);

View File

@ -38,16 +38,6 @@ export interface TakePhotoOptions {
* @default false
*/
enableAutoDistortionCorrection?: boolean;
/**
* When set to `true`, metadata reading and mapping will be skipped. ({@linkcode PhotoFile.metadata} will be null)
*
* This might result in a faster capture, as metadata reading and mapping requires File IO.
*
* @default false
*
* @platform Android
*/
skipMetadata?: boolean;
}
/**
@ -80,10 +70,11 @@ export interface PhotoFile extends TemporaryFile {
isMirrored: boolean;
thumbnail?: Record<string, unknown>;
/**
* Metadata information describing the captured image.
* Metadata information describing the captured image. (iOS only)
*
* @see [AVCapturePhoto.metadata](https://developer.apple.com/documentation/avfoundation/avcapturephoto/2873982-metadata)
* @see [AndroidX ExifInterface](https://developer.android.com/reference/androidx/exifinterface/media/ExifInterface)
*
* @platform iOS
*/
metadata?: {
/**