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 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)
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
|
@ -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",
|
||||||
|
@ -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()
|
||||||
|
@ -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} " +
|
||||||
|
@ -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 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."
|
||||||
}
|
}
|
||||||
|
@ -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)")))
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
Loading…
Reference in New Issue
Block a user