fix: Catch insufficient-storage
errors (#2422)
* fix: Catch `insufficient-storage` errors * feat: Implement `insufficient-storage` error for Android * fix: Catch insufficient storage error also on takePhoto android
This commit is contained in:
parent
7894779094
commit
b1fa06514f
@ -5,8 +5,8 @@ import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.facebook.react.bridge.*
|
||||
import com.mrousavy.camera.core.CameraError
|
||||
import com.mrousavy.camera.core.MicrophonePermissionError
|
||||
import com.mrousavy.camera.core.RecorderError
|
||||
import com.mrousavy.camera.core.RecordingSession
|
||||
import com.mrousavy.camera.core.code
|
||||
import com.mrousavy.camera.types.RecordVideoOptions
|
||||
@ -29,7 +29,7 @@ suspend fun CameraView.startRecording(options: RecordVideoOptions, onRecordCallb
|
||||
map.putInt("height", video.size.height)
|
||||
onRecordCallback(map, null)
|
||||
}
|
||||
val onError = { error: RecorderError ->
|
||||
val onError = { error: CameraError ->
|
||||
val errorMap = makeErrorMap(error.code, error.message)
|
||||
onRecordCallback(null, errorMap)
|
||||
}
|
||||
|
@ -12,11 +12,13 @@ import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.WritableMap
|
||||
import com.mrousavy.camera.core.CameraSession
|
||||
import com.mrousavy.camera.core.InsufficientStorageError
|
||||
import com.mrousavy.camera.types.Flash
|
||||
import com.mrousavy.camera.types.QualityPrioritization
|
||||
import com.mrousavy.camera.utils.*
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
private const val TAG = "CameraView.takePhoto"
|
||||
@ -49,7 +51,15 @@ suspend fun CameraView.takePhoto(optionsMap: ReadableMap): WritableMap {
|
||||
|
||||
val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId!!)
|
||||
|
||||
val path = savePhotoToFile(context, cameraCharacteristics, photo)
|
||||
val path = try {
|
||||
savePhotoToFile(context, cameraCharacteristics, photo)
|
||||
} catch (e: IOException) {
|
||||
if (e.message?.contains("no space left", true) == true) {
|
||||
throw InsufficientStorageError()
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Successfully saved photo to file! $path")
|
||||
|
||||
@ -93,7 +103,7 @@ private suspend fun savePhotoToFile(
|
||||
when (photo.format) {
|
||||
// When the format is JPEG or DEPTH JPEG we can simply save the bytes as-is
|
||||
ImageFormat.JPEG, ImageFormat.DEPTH_JPEG -> {
|
||||
val file = createFile(context, ".jpg")
|
||||
val file = FileUtils.createTempFile(context, ".jpg")
|
||||
writePhotoToFile(photo, file)
|
||||
return@withContext file.absolutePath
|
||||
}
|
||||
@ -101,7 +111,7 @@ private suspend fun savePhotoToFile(
|
||||
// When the format is RAW we use the DngCreator utility library
|
||||
ImageFormat.RAW_SENSOR -> {
|
||||
val dngCreator = DngCreator(cameraCharacteristics, photo.metadata)
|
||||
val file = createFile(context, ".dng")
|
||||
val file = FileUtils.createTempFile(context, ".dng")
|
||||
FileOutputStream(file).use { stream ->
|
||||
// TODO: Make sure orientation is loaded properly here?
|
||||
dngCreator.writeImage(stream, photo.image)
|
||||
@ -114,8 +124,3 @@ private suspend fun savePhotoToFile(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFile(context: Context, extension: String): File =
|
||||
File.createTempFile("mrousavy", extension, context.cacheDir).apply {
|
||||
deleteOnExit()
|
||||
}
|
||||
|
@ -106,6 +106,7 @@ 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 InsufficientStorageError : CameraError("capture", "insufficient-storage", "There is not enough storage space available.")
|
||||
class RecordingInProgressError :
|
||||
CameraError(
|
||||
"capture",
|
||||
|
@ -623,7 +623,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
|
||||
enableAudio: Boolean,
|
||||
options: RecordVideoOptions,
|
||||
callback: (video: RecordingSession.Video) -> Unit,
|
||||
onError: (error: RecorderError) -> Unit
|
||||
onError: (error: CameraError) -> Unit
|
||||
) {
|
||||
mutex.withLock {
|
||||
if (recording != null) throw RecordingInProgressError()
|
||||
|
@ -7,9 +7,11 @@ import android.os.Build
|
||||
import android.util.Log
|
||||
import android.util.Size
|
||||
import android.view.Surface
|
||||
import com.facebook.common.statfs.StatFsHelper
|
||||
import com.mrousavy.camera.extensions.getRecommendedBitRate
|
||||
import com.mrousavy.camera.types.Orientation
|
||||
import com.mrousavy.camera.types.RecordVideoOptions
|
||||
import com.mrousavy.camera.utils.FileUtils
|
||||
import java.io.File
|
||||
|
||||
class RecordingSession(
|
||||
@ -22,7 +24,7 @@ class RecordingSession(
|
||||
private val orientation: Orientation,
|
||||
private val options: RecordVideoOptions,
|
||||
private val callback: (video: Video) -> Unit,
|
||||
private val onError: (error: RecorderError) -> Unit
|
||||
private val onError: (error: CameraError) -> Unit
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "RecordingSession"
|
||||
@ -42,7 +44,7 @@ class RecordingSession(
|
||||
|
||||
// TODO: Implement HDR
|
||||
init {
|
||||
outputFile = File.createTempFile("mrousavy", options.fileType.toExtension(), context.cacheDir)
|
||||
outputFile = FileUtils.createTempFile(context, options.fileType.toExtension())
|
||||
|
||||
Log.i(TAG, "Creating RecordingSession for ${outputFile.absolutePath}")
|
||||
|
||||
@ -52,9 +54,11 @@ class RecordingSession(
|
||||
recorder.setVideoSource(MediaRecorder.VideoSource.SURFACE)
|
||||
|
||||
recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
|
||||
|
||||
recorder.setOutputFile(outputFile.absolutePath)
|
||||
recorder.setVideoEncodingBitRate(bitRate)
|
||||
recorder.setVideoSize(size.height, size.width)
|
||||
recorder.setMaxFileSize(getMaxFileSize())
|
||||
if (fps != null) recorder.setVideoFrameRate(fps)
|
||||
|
||||
Log.i(TAG, "Using ${options.videoCodec} Video Codec at ${bitRate / 1_000_000.0} Mbps..")
|
||||
@ -81,6 +85,9 @@ class RecordingSession(
|
||||
}
|
||||
recorder.setOnInfoListener { _, what, extra ->
|
||||
Log.i(TAG, "MediaRecorder Info: $what ($extra)")
|
||||
if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
|
||||
onError(InsufficientStorageError())
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "Created $this!")
|
||||
@ -142,6 +149,13 @@ class RecordingSession(
|
||||
return bitRate
|
||||
}
|
||||
|
||||
private fun getMaxFileSize(): Long {
|
||||
val statFs = StatFsHelper.getInstance()
|
||||
val availableStorage = statFs.getAvailableStorageSpace(StatFsHelper.StorageType.INTERNAL)
|
||||
Log.i(TAG, "Maximum available storage space: ${availableStorage / 1_000} kB")
|
||||
return availableStorage
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val audio = if (enableAudio) "with audio" else "without audio"
|
||||
return "${size.width} x ${size.height} @ $fps FPS ${options.videoCodec} ${options.fileType} " +
|
||||
|
@ -0,0 +1,13 @@
|
||||
package com.mrousavy.camera.utils
|
||||
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
|
||||
class FileUtils {
|
||||
companion object {
|
||||
fun createTempFile(context: Context, extension: String): File =
|
||||
File.createTempFile("mrousavy", extension, context.cacheDir).also {
|
||||
it.deleteOnExit()
|
||||
}
|
||||
}
|
||||
}
|
@ -180,6 +180,7 @@ enum CaptureError {
|
||||
case videoNotEnabled
|
||||
case photoNotEnabled
|
||||
case aborted
|
||||
case insufficientStorage
|
||||
case unknown(message: String? = nil)
|
||||
|
||||
var code: String {
|
||||
@ -198,6 +199,8 @@ enum CaptureError {
|
||||
return "video-not-enabled"
|
||||
case .photoNotEnabled:
|
||||
return "photo-not-enabled"
|
||||
case .insufficientStorage:
|
||||
return "insufficient-storage"
|
||||
case .aborted:
|
||||
return "aborted"
|
||||
case .unknown:
|
||||
@ -223,6 +226,8 @@ enum CaptureError {
|
||||
return "Photo capture is disabled! Pass `photo={true}` to enable photo capture."
|
||||
case .aborted:
|
||||
return "The capture has been stopped before any input data arrived."
|
||||
case .insufficientStorage:
|
||||
return "There is not enough storage space available."
|
||||
case let .unknown(message: message):
|
||||
return message ?? "An unknown error occured while capturing a video/photo."
|
||||
}
|
||||
|
@ -54,6 +54,8 @@ extension CameraSession {
|
||||
// Something went wrong, we have an error
|
||||
if error.domain == "capture/aborted" {
|
||||
onError(.capture(.aborted))
|
||||
} else if error.code == -11807 {
|
||||
onError(.capture(.insufficientStorage))
|
||||
} else {
|
||||
onError(.capture(.unknown(message: "An unknown recording error occured! \(error.code) \(error.description)")))
|
||||
}
|
||||
|
@ -84,7 +84,11 @@ class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
|
||||
delegatesReferences.removeAll(where: { $0 == self })
|
||||
}
|
||||
if let error = error as NSError? {
|
||||
promise.reject(error: .capture(.unknown(message: error.description)), cause: error)
|
||||
if error.code == -11807 {
|
||||
promise.reject(error: .capture(.insufficientStorage), cause: error)
|
||||
} else {
|
||||
promise.reject(error: .capture(.unknown(message: error.description)), cause: error)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ export type CaptureError =
|
||||
| 'capture/file-io-error'
|
||||
| 'capture/create-temp-file-error'
|
||||
| 'capture/create-recorder-error'
|
||||
| 'capture/insufficient-storage'
|
||||
| 'capture/recorder-error'
|
||||
| 'capture/video-not-enabled'
|
||||
| 'capture/photo-not-enabled'
|
||||
|
Loading…
Reference in New Issue
Block a user