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:
Marc Rousavy 2024-01-24 11:48:38 +01:00 committed by GitHub
parent 7894779094
commit b1fa06514f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 59 additions and 14 deletions

View File

@ -5,8 +5,8 @@ import android.annotation.SuppressLint
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.facebook.react.bridge.* import com.facebook.react.bridge.*
import com.mrousavy.camera.core.CameraError
import com.mrousavy.camera.core.MicrophonePermissionError import com.mrousavy.camera.core.MicrophonePermissionError
import com.mrousavy.camera.core.RecorderError
import com.mrousavy.camera.core.RecordingSession import com.mrousavy.camera.core.RecordingSession
import com.mrousavy.camera.core.code import com.mrousavy.camera.core.code
import com.mrousavy.camera.types.RecordVideoOptions import com.mrousavy.camera.types.RecordVideoOptions
@ -29,7 +29,7 @@ suspend fun CameraView.startRecording(options: RecordVideoOptions, onRecordCallb
map.putInt("height", video.size.height) map.putInt("height", video.size.height)
onRecordCallback(map, null) onRecordCallback(map, null)
} }
val onError = { error: RecorderError -> val onError = { error: CameraError ->
val errorMap = makeErrorMap(error.code, error.message) val errorMap = makeErrorMap(error.code, error.message)
onRecordCallback(null, errorMap) onRecordCallback(null, errorMap)
} }

View File

@ -12,11 +12,13 @@ import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableMap
import com.mrousavy.camera.core.CameraSession import com.mrousavy.camera.core.CameraSession
import com.mrousavy.camera.core.InsufficientStorageError
import com.mrousavy.camera.types.Flash import com.mrousavy.camera.types.Flash
import com.mrousavy.camera.types.QualityPrioritization import com.mrousavy.camera.types.QualityPrioritization
import com.mrousavy.camera.utils.* import com.mrousavy.camera.utils.*
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException
import kotlinx.coroutines.* import kotlinx.coroutines.*
private const val TAG = "CameraView.takePhoto" private const val TAG = "CameraView.takePhoto"
@ -49,7 +51,15 @@ suspend fun CameraView.takePhoto(optionsMap: ReadableMap): WritableMap {
val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId!!) 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") Log.i(TAG, "Successfully saved photo to file! $path")
@ -93,7 +103,7 @@ private suspend fun savePhotoToFile(
when (photo.format) { when (photo.format) {
// When the format is JPEG or DEPTH JPEG we can simply save the bytes as-is // When the format is JPEG or DEPTH JPEG we can simply save the bytes as-is
ImageFormat.JPEG, ImageFormat.DEPTH_JPEG -> { ImageFormat.JPEG, ImageFormat.DEPTH_JPEG -> {
val file = createFile(context, ".jpg") val file = FileUtils.createTempFile(context, ".jpg")
writePhotoToFile(photo, file) writePhotoToFile(photo, file)
return@withContext file.absolutePath return@withContext file.absolutePath
} }
@ -101,7 +111,7 @@ private suspend fun savePhotoToFile(
// When the format is RAW we use the DngCreator utility library // When the format is RAW we use the DngCreator utility library
ImageFormat.RAW_SENSOR -> { ImageFormat.RAW_SENSOR -> {
val dngCreator = DngCreator(cameraCharacteristics, photo.metadata) val dngCreator = DngCreator(cameraCharacteristics, photo.metadata)
val file = createFile(context, ".dng") val file = FileUtils.createTempFile(context, ".dng")
FileOutputStream(file).use { stream -> FileOutputStream(file).use { stream ->
// TODO: Make sure orientation is loaded properly here? // TODO: Make sure orientation is loaded properly here?
dngCreator.writeImage(stream, photo.image) 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()
}

View File

@ -106,6 +106,7 @@ class RecorderError(name: String, extra: Int) :
CameraError("capture", "recorder-error", "An error occured while recording a video! $name $extra") CameraError("capture", "recorder-error", "An error occured while recording a video! $name $extra")
class NoRecordingInProgressError : class NoRecordingInProgressError :
CameraError("capture", "no-recording-in-progress", "There was no active video recording in progress! Did you call stopRecording() twice?") 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 : class RecordingInProgressError :
CameraError( CameraError(
"capture", "capture",

View File

@ -623,7 +623,7 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
enableAudio: Boolean, enableAudio: Boolean,
options: RecordVideoOptions, options: RecordVideoOptions,
callback: (video: RecordingSession.Video) -> Unit, callback: (video: RecordingSession.Video) -> Unit,
onError: (error: RecorderError) -> Unit onError: (error: CameraError) -> Unit
) { ) {
mutex.withLock { mutex.withLock {
if (recording != null) throw RecordingInProgressError() if (recording != null) throw RecordingInProgressError()

View File

@ -7,9 +7,11 @@ 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.facebook.common.statfs.StatFsHelper
import com.mrousavy.camera.extensions.getRecommendedBitRate import com.mrousavy.camera.extensions.getRecommendedBitRate
import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.Orientation
import com.mrousavy.camera.types.RecordVideoOptions import com.mrousavy.camera.types.RecordVideoOptions
import com.mrousavy.camera.utils.FileUtils
import java.io.File import java.io.File
class RecordingSession( class RecordingSession(
@ -22,7 +24,7 @@ class RecordingSession(
private val orientation: Orientation, private val orientation: Orientation,
private val options: RecordVideoOptions, private val options: RecordVideoOptions,
private val callback: (video: Video) -> Unit, private val callback: (video: Video) -> Unit,
private val onError: (error: RecorderError) -> Unit private val onError: (error: CameraError) -> Unit
) { ) {
companion object { companion object {
private const val TAG = "RecordingSession" private const val TAG = "RecordingSession"
@ -42,7 +44,7 @@ class RecordingSession(
// TODO: Implement HDR // TODO: Implement HDR
init { 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}") Log.i(TAG, "Creating RecordingSession for ${outputFile.absolutePath}")
@ -52,9 +54,11 @@ class RecordingSession(
recorder.setVideoSource(MediaRecorder.VideoSource.SURFACE) recorder.setVideoSource(MediaRecorder.VideoSource.SURFACE)
recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
recorder.setOutputFile(outputFile.absolutePath) recorder.setOutputFile(outputFile.absolutePath)
recorder.setVideoEncodingBitRate(bitRate) recorder.setVideoEncodingBitRate(bitRate)
recorder.setVideoSize(size.height, size.width) recorder.setVideoSize(size.height, size.width)
recorder.setMaxFileSize(getMaxFileSize())
if (fps != null) recorder.setVideoFrameRate(fps) if (fps != null) recorder.setVideoFrameRate(fps)
Log.i(TAG, "Using ${options.videoCodec} Video Codec at ${bitRate / 1_000_000.0} Mbps..") 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 -> recorder.setOnInfoListener { _, what, extra ->
Log.i(TAG, "MediaRecorder Info: $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!") Log.i(TAG, "Created $this!")
@ -142,6 +149,13 @@ class RecordingSession(
return bitRate 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 { override fun toString(): String {
val audio = if (enableAudio) "with audio" else "without audio" val audio = if (enableAudio) "with audio" else "without audio"
return "${size.width} x ${size.height} @ $fps FPS ${options.videoCodec} ${options.fileType} " + return "${size.width} x ${size.height} @ $fps FPS ${options.videoCodec} ${options.fileType} " +

View File

@ -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()
}
}
}

View File

@ -180,6 +180,7 @@ enum CaptureError {
case videoNotEnabled case videoNotEnabled
case photoNotEnabled case photoNotEnabled
case aborted case aborted
case insufficientStorage
case unknown(message: String? = nil) case unknown(message: String? = nil)
var code: String { var code: String {
@ -198,6 +199,8 @@ enum CaptureError {
return "video-not-enabled" return "video-not-enabled"
case .photoNotEnabled: case .photoNotEnabled:
return "photo-not-enabled" return "photo-not-enabled"
case .insufficientStorage:
return "insufficient-storage"
case .aborted: case .aborted:
return "aborted" return "aborted"
case .unknown: case .unknown:
@ -223,6 +226,8 @@ enum CaptureError {
return "Photo capture is disabled! Pass `photo={true}` to enable photo capture." return "Photo capture is disabled! Pass `photo={true}` to enable photo capture."
case .aborted: case .aborted:
return "The capture has been stopped before any input data arrived." 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): case let .unknown(message: message):
return message ?? "An unknown error occured while capturing a video/photo." return message ?? "An unknown error occured while capturing a video/photo."
} }

View File

@ -54,6 +54,8 @@ extension CameraSession {
// Something went wrong, we have an error // Something went wrong, we have an error
if error.domain == "capture/aborted" { if error.domain == "capture/aborted" {
onError(.capture(.aborted)) onError(.capture(.aborted))
} else if error.code == -11807 {
onError(.capture(.insufficientStorage))
} else { } else {
onError(.capture(.unknown(message: "An unknown recording error occured! \(error.code) \(error.description)"))) onError(.capture(.unknown(message: "An unknown recording error occured! \(error.code) \(error.description)")))
} }

View File

@ -84,7 +84,11 @@ class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
delegatesReferences.removeAll(where: { $0 == self }) delegatesReferences.removeAll(where: { $0 == self })
} }
if let error = error as NSError? { 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 return
} }
} }

View File

@ -36,6 +36,7 @@ export type CaptureError =
| 'capture/file-io-error' | 'capture/file-io-error'
| 'capture/create-temp-file-error' | 'capture/create-temp-file-error'
| 'capture/create-recorder-error' | 'capture/create-recorder-error'
| 'capture/insufficient-storage'
| 'capture/recorder-error' | 'capture/recorder-error'
| 'capture/video-not-enabled' | 'capture/video-not-enabled'
| 'capture/photo-not-enabled' | 'capture/photo-not-enabled'