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, suspend fun startRecording(enableAudio: Boolean,
codec: VideoCodec, codec: VideoCodec,
fileType: VideoFileType, fileType: VideoFileType,
callback: (video: RecordingSession.Video) -> Unit) { callback: (video: RecordingSession.Video) -> Unit,
onError: (error: RecorderError) -> Unit) {
mutex.withLock { mutex.withLock {
if (recording != null) throw RecordingInProgressError() if (recording != null) throw RecordingInProgressError()
val outputs = outputs ?: throw CameraNotReadyError() val outputs = outputs ?: throw CameraNotReadyError()
val videoOutput = outputs.videoOutput ?: throw VideoNotEnabledError() 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() recording.start()
this.recording = recording 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.VideoCodec
import com.mrousavy.camera.parsers.VideoFileType import com.mrousavy.camera.parsers.VideoFileType
import com.mrousavy.camera.utils.RecordingSession import com.mrousavy.camera.utils.RecordingSession
import com.mrousavy.camera.utils.makeErrorMap
import java.util.* import java.util.*
suspend fun CameraView.startRecording(options: ReadableMap, onRecordCallback: Callback) { 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) map.putDouble("duration", video.durationMs.toDouble() / 1000.0)
onRecordCallback(map, null) 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") @SuppressLint("RestrictedApi")

View File

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

View File

@ -1,6 +1,5 @@
package com.mrousavy.camera package com.mrousavy.camera
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.MapBuilder import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ThemedReactContext
@ -13,7 +12,7 @@ import com.mrousavy.camera.parsers.Torch
import com.mrousavy.camera.parsers.VideoStabilizationMode import com.mrousavy.camera.parsers.VideoStabilizationMode
@Suppress("unused") @Suppress("unused")
class CameraViewManager(reactContext: ReactApplicationContext) : ViewGroupManager<CameraView>() { class CameraViewManager() : ViewGroupManager<CameraView>() {
public override fun createViewInstance(context: ThemedReactContext): CameraView { public override fun createViewInstance(context: ThemedReactContext): CameraView {
return CameraView(context) 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 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 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( class HdrNotContainedInFormatError : CameraError(
"format", "invalid-hdr", "format", "invalid-hdr",
"The currently selected format does not support HDR capture! " + "The currently selected format does not support HDR capture! " +
"Make sure you select a format which includes `supportsPhotoHDR`!" "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 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") 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 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 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" + class RecorderError(name: String, extra: Int) : CameraError("capture", "recorder-error", "An error occured while recording a video! $name $extra")
"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 NoRecordingInProgressError : CameraError("capture", "no-recording-in-progress", "There was no active video recording in progress! Did you call stopRecording() twice?") 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?") 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 * 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 else
this.putDouble(key, value) 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"), NATIVE("native"),
UNKNOWN("unknown"); 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 { fun toImageFormat(): Int {
val result = when (this) { val result = when (this) {
YUV -> ImageFormat.YUV_420_888 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.Log
import android.util.Size import android.util.Size
import android.view.Surface import android.view.Surface
import com.mrousavy.camera.RecorderError
import com.mrousavy.camera.parsers.Orientation import com.mrousavy.camera.parsers.Orientation
import com.mrousavy.camera.parsers.VideoCodec import com.mrousavy.camera.parsers.VideoCodec
import com.mrousavy.camera.parsers.VideoFileType import com.mrousavy.camera.parsers.VideoFileType
@ -22,7 +23,8 @@ class RecordingSession(context: Context,
private val codec: VideoCodec = VideoCodec.H264, private val codec: VideoCodec = VideoCodec.H264,
private val orientation: Orientation, private val orientation: Orientation,
private val fileType: VideoFileType = VideoFileType.MP4, 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 { companion object {
private const val TAG = "RecordingSession" private const val TAG = "RecordingSession"
// bits per second // bits per second
@ -77,6 +79,12 @@ class RecordingSession(context: Context,
recorder.setOnErrorListener { _, what, extra -> recorder.setOnErrorListener { _, what, extra ->
Log.e(TAG, "MediaRecorder Error: $what ($extra)") Log.e(TAG, "MediaRecorder Error: $what ($extra)")
stop() 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 -> recorder.setOnInfoListener { _, what, extra ->
Log.i(TAG, "MediaRecorder Info: $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.graphics.ImageFormat
import android.hardware.HardwareBuffer import android.hardware.HardwareBuffer
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager import android.hardware.camera2.CameraManager
import android.media.Image import android.media.Image
import android.media.ImageReader import android.media.ImageReader
import android.os.Build
import android.util.Log import android.util.Log
import android.util.Size import android.util.Size
import android.view.Surface import android.view.Surface
@ -92,7 +92,6 @@ class CameraOutputs(val cameraId: String,
init { init {
val characteristics = cameraManager.getCameraCharacteristics(cameraId) val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val config = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
Log.i(TAG, "Preparing Outputs for Camera $cameraId...") 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()) // Video output: High resolution repeating images (startRecording() or useFrameProcessor())
if (video != null) { 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 size = characteristics.getVideoSizes(cameraId, video.format).closestToOrMax(video.targetSize)
val flags = HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE or HardwareBuffer.USAGE_VIDEO_ENCODE 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', qualityPrioritization: 'speed',
flash: flash, flash: flash,
quality: 90, quality: 90,
skipMetadata: true,
}), }),
[flash], [flash],
); );

View File

@ -38,16 +38,6 @@ export interface TakePhotoOptions {
* @default false * @default false
*/ */
enableAutoDistortionCorrection?: boolean; 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; isMirrored: boolean;
thumbnail?: Record<string, unknown>; 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 [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?: { metadata?: {
/** /**