From b1fa06514f4f2450ca2d0387d2ec781963ed4ef8 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Wed, 24 Jan 2024 11:48:38 +0100 Subject: [PATCH] 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 --- .../mrousavy/camera/CameraView+RecordVideo.kt | 4 ++-- .../mrousavy/camera/CameraView+TakePhoto.kt | 21 ++++++++++++------- .../com/mrousavy/camera/core/CameraError.kt | 1 + .../com/mrousavy/camera/core/CameraSession.kt | 2 +- .../mrousavy/camera/core/RecordingSession.kt | 18 ++++++++++++++-- .../com/mrousavy/camera/utils/FileUtils.kt | 13 ++++++++++++ package/ios/Core/CameraError.swift | 5 +++++ package/ios/Core/CameraSession+Video.swift | 2 ++ package/ios/Core/PhotoCaptureDelegate.swift | 6 +++++- package/src/CameraError.ts | 1 + 10 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 package/android/src/main/java/com/mrousavy/camera/utils/FileUtils.kt diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView+RecordVideo.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView+RecordVideo.kt index 53642c8..0b06ba6 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView+RecordVideo.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView+RecordVideo.kt @@ -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) } diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt index 88c085f..7ba6775 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt @@ -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() - } diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt index 9936ec3..70dc1ed 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt @@ -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", diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt index 2991b71..df14fd2 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession.kt @@ -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() diff --git a/package/android/src/main/java/com/mrousavy/camera/core/RecordingSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/RecordingSession.kt index 3b481d2..78e2f14 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/RecordingSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/RecordingSession.kt @@ -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} " + diff --git a/package/android/src/main/java/com/mrousavy/camera/utils/FileUtils.kt b/package/android/src/main/java/com/mrousavy/camera/utils/FileUtils.kt new file mode 100644 index 0000000..f3b462b --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/utils/FileUtils.kt @@ -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() + } + } +} diff --git a/package/ios/Core/CameraError.swift b/package/ios/Core/CameraError.swift index 46235de..e460c32 100644 --- a/package/ios/Core/CameraError.swift +++ b/package/ios/Core/CameraError.swift @@ -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." } diff --git a/package/ios/Core/CameraSession+Video.swift b/package/ios/Core/CameraSession+Video.swift index 3edf5e5..00ff941 100644 --- a/package/ios/Core/CameraSession+Video.swift +++ b/package/ios/Core/CameraSession+Video.swift @@ -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)"))) } diff --git a/package/ios/Core/PhotoCaptureDelegate.swift b/package/ios/Core/PhotoCaptureDelegate.swift index 84d6496..7416bb9 100644 --- a/package/ios/Core/PhotoCaptureDelegate.swift +++ b/package/ios/Core/PhotoCaptureDelegate.swift @@ -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 } } diff --git a/package/src/CameraError.ts b/package/src/CameraError.ts index 1f34280..94b205a 100644 --- a/package/src/CameraError.ts +++ b/package/src/CameraError.ts @@ -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'