fix: Clean up codebase
This commit is contained in:
parent
07ba0e1a41
commit
e1b04088c6
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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?")
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)")
|
||||
|
@ -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
|
||||
|
@ -64,7 +64,6 @@ const _CaptureButton: React.FC<Props> = ({
|
||||
qualityPrioritization: 'speed',
|
||||
flash: flash,
|
||||
quality: 90,
|
||||
skipMetadata: true,
|
||||
}),
|
||||
[flash],
|
||||
);
|
||||
|
@ -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?: {
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user