Skip to content

Commit

Permalink
save path
Browse files Browse the repository at this point in the history
  • Loading branch information
j-jonathan authored and Jonathan JADLO committed May 29, 2024
1 parent 08bf8ed commit d263742
Show file tree
Hide file tree
Showing 19 changed files with 143 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ class FrameInvalidError :
"- If you want to use runOnJS, increment it's ref-count: `frame.incrementRefCount()`"
)
class InvalidImageTypeError : CameraError("capture", "invalid-image-type", "Captured an Image with an invalid Image type!")
class InvalidPathError(message: String) :
CameraError("capture", "invalid-path", "The given path is invalid! $message")

class CodeTypeNotSupportedError(codeType: String) :
CameraError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ class CameraSession(private val context: Context, private val callback: Callback
}
}

suspend fun takePhoto(flash: Flash, enableShutterSound: Boolean, outputOrientation: Orientation): Photo {
suspend fun takePhoto(flash: Flash, enableShutterSound: Boolean, outputOrientation: Orientation, path: String?): Photo {
val camera = camera ?: throw CameraNotReadyError()
val photoOutput = photoOutput ?: throw PhotoNotEnabledError()

Expand All @@ -504,7 +504,7 @@ class CameraSession(private val context: Context, private val callback: Callback
photoOutput.targetRotation = outputOrientation.toSurfaceRotation()
val enableShutterSoundActual = getEnableShutterSoundActual(enableShutterSound)

val photoFile = photoOutput.takePicture(context, enableShutterSoundActual, metadataProvider, callback, CameraQueues.cameraExecutor)
val photoFile = photoOutput.takePicture(context, enableShutterSoundActual, metadataProvider, callback, CameraQueues.cameraExecutor, path)
val isMirrored = photoFile.metadata.isReversedHorizontal

val bitmapOptions = BitmapFactory.Options().also {
Expand Down Expand Up @@ -538,7 +538,7 @@ class CameraSession(private val context: Context, private val callback: Callback
if (recording != null) throw RecordingInProgressError()
val videoOutput = videoOutput ?: throw VideoNotEnabledError()

val file = FileUtils.createTempFile(context, options.fileType.toExtension())
val file = FileUtils.getDestinationFile(context, options.path, options.fileType.toExtension())
val outputOptions = FileOutputOptions.Builder(file).also { outputOptions ->
metadataProvider.location?.let { location ->
Log.i(TAG, "Setting Video Location to ${location.latitude}, ${location.longitude}...")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ suspend inline fun ImageCapture.takePicture(
enableShutterSound: Boolean,
metadataProvider: MetadataProvider,
callback: CameraSession.Callback,
executor: Executor
executor: Executor,
path: String?
): PhotoFileInfo =
suspendCancellableCoroutine { continuation ->
// Shutter sound
val shutterSound = if (enableShutterSound) MediaActionSound() else null
shutterSound?.load(MediaActionSound.SHUTTER_CLICK)

val file = FileUtils.createTempFile(context, ".jpg")
val file = FileUtils.getDestinationFile(context, path, ".jpg")
val outputFileOptionsBuilder = OutputFileOptions.Builder(file).also { options ->
val metadata = ImageCapture.Metadata()
metadataProvider.location?.let { location ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.facebook.react.bridge.ReadableMap

class RecordVideoOptions(map: ReadableMap) {
var fileType: VideoFileType = VideoFileType.MOV
var path: String? = null
var videoCodec = VideoCodec.H264
var videoBitRateOverride: Double? = null
var videoBitRateMultiplier: Double? = null
Expand All @@ -12,6 +13,9 @@ class RecordVideoOptions(map: ReadableMap) {
if (map.hasKey("fileType")) {
fileType = VideoFileType.fromUnionValue(map.getString("fileType"))
}
if (map.hasKey("path")) {
path = map.getString("path")
}
if (map.hasKey("videoCodec")) {
videoCodec = VideoCodec.fromUnionValue(map.getString("videoCodec"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package com.mrousavy.camera.core.types

import com.facebook.react.bridge.ReadableMap

data class SnapshotOptions(val quality: Int) {
data class SnapshotOptions(val quality: Int, val path: String?) {
companion object {
fun fromJSValue(options: ReadableMap): SnapshotOptions {
val quality = options.getInt("quality")
return SnapshotOptions(quality)
val quality = if (options.hasKey("quality")) options.getInt("quality") else 100
val path = options.getString("path")
return SnapshotOptions(quality, path)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.mrousavy.camera.core.utils

import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import com.mrousavy.camera.core.InvalidPathError
import java.io.File
import java.io.FileOutputStream

Expand All @@ -12,10 +14,36 @@ class FileUtils {
it.deleteOnExit()
}

fun writeBitmapTofile(bitmap: Bitmap, file: File, quality: Int = 100) {
fun writeBitmapTofile(bitmap: Bitmap, file: File, quality: Int) {
FileOutputStream(file).use { stream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream)
}
}

fun getDestinationFile(context: Context, path: String?, fileExtension: String): File {
val destinationPath = Uri.parse(path).path
if (destinationPath != null) {
val destinationFile = File(destinationPath)
// Check if the directory exists
if (!destinationFile.parentFile.exists()) {
throw InvalidPathError("Directory does not exist: ${destinationFile.parentFile.path}")
}
// Check if the directory is a directory
if (!destinationFile.parentFile.isDirectory) {
throw InvalidPathError("Path directory is not a directory: ${destinationFile.parentFile.path}")
}
// Check if the directory is readable and writable
if (!destinationFile.parentFile.canRead() || !destinationFile.parentFile.canWrite()) {
throw InvalidPathError("Path directory is not readable or writable: ${destinationFile.parentFile.path}")
}
// Check if the path doesn't exist
if (destinationFile.exists()) {
throw InvalidPathError("File already exists at path: $path")
}
return destinationFile
} else {
return createTempFile(context, fileExtension)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ suspend fun CameraView.takePhoto(optionsMap: ReadableMap): WritableMap {
val photo = cameraSession.takePhoto(
Flash.fromUnionValue(flash),
enableShutterSound,
orientation
orientation,
options["path"] as? String
)

Log.i(TAG, "Successfully captured ${photo.width} x ${photo.height} photo!")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ fun CameraView.takeSnapshot(options: SnapshotOptions): WritableMap {

onShutter(ShutterType.SNAPSHOT)

val file = FileUtils.createTempFile(context, ".jpg")
val file = FileUtils.getDestinationFile(context, options.path, ".jpg")
FileUtils.writeBitmapTofile(bitmap, file, options.quality)

Log.i(TAG, "Successfully saved snapshot to file!")
Expand Down
5 changes: 5 additions & 0 deletions package/ios/Core/CameraError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ enum CaptureError {
case timedOut
case insufficientStorage
case failedWritingMetadata(cause: Error?)
case invalidPath(message: String? = nil)
case unknown(message: String? = nil)

var code: String {
Expand Down Expand Up @@ -251,6 +252,8 @@ enum CaptureError {
return "insufficient-storage"
case .failedWritingMetadata:
return "failed-writing-metadata"
case .invalidPath:
return "invalid-path"
case .unknown:
return "unknown"
}
Expand Down Expand Up @@ -286,6 +289,8 @@ enum CaptureError {
return "Failed to write video/photo metadata! (Cause: \(cause?.localizedDescription ?? "unknown"))"
case .insufficientStorage:
return "There is not enough storage space available."
case let .invalidPath(message: message):
return "The given path is invalid! \(message ?? "(no additional message)")"
case let .unknown(message: message):
return message ?? "An unknown error occured while capturing a video/photo."
}
Expand Down
14 changes: 14 additions & 0 deletions package/ios/Core/CameraSession+Photo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,22 @@ extension CameraSession {
// shutter sound
let enableShutterSound = options["enableShutterSound"] as? Bool ?? true

// Destination URL
let destinationURL: URL
do {
destinationURL = try FileUtils.getDestinationURL(path: options["path"] as? String, fileExtension: "jpeg")
VisionLogger.log(level: .info, message: "Will record to file: \(destinationURL)")
} catch let error as CameraError {
promise.reject(error: error)
return
} catch {
promise.reject(error: .unknown(message: "An unknown error occurred while getting the destination URL."))
return
}

// Actually do the capture!
let photoCaptureDelegate = PhotoCaptureDelegate(promise: promise,
path: destinationURL,
enableShutterSound: enableShutterSound,
metadataProvider: self.metadataProvider,
cameraSessionDelegate: self.delegate)
Expand Down
20 changes: 11 additions & 9 deletions package/ios/Core/CameraSession+Video.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,21 +86,23 @@ extension CameraSession {
}
}

// Create temporary file
let errorPointer = ErrorPointer(nilLiteral: ())
// Destination URL
let fileExtension = options.fileType.descriptor ?? "mov"
guard let tempFilePath = RCTTempFilePath(fileExtension, errorPointer) else {
let message = errorPointer?.pointee?.description
onError(.capture(.createTempFileError(message: message)))
let destinationURL: URL
do {
destinationURL = try FileUtils.getDestinationURL(path: options.path, fileExtension: fileExtension)
VisionLogger.log(level: .info, message: "Will record to file: \(destinationURL)")
} catch let error as CameraError {
onError(error)
return
} catch {
onError(.unknown(message: "An unknown error occurred while getting the destination URL."))
return
}

VisionLogger.log(level: .info, message: "Will record to temporary file: \(tempFilePath)")
let tempURL = URL(string: "file://\(tempFilePath)")!

do {
// Create RecordingSession for the temp file
let recordingSession = try RecordingSession(url: tempURL,
let recordingSession = try RecordingSession(url: destinationURL,
fileType: options.fileType,
metadataProvider: self.metadataProvider,
completion: onFinish)
Expand Down
5 changes: 4 additions & 1 deletion package/ios/Core/PhotoCaptureDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@ import AVFoundation

class PhotoCaptureDelegate: GlobalReferenceHolder, AVCapturePhotoCaptureDelegate {
private let promise: Promise
private let path: URL
private let enableShutterSound: Bool
private let cameraSessionDelegate: CameraSessionDelegate?
private let metadataProvider: MetadataProvider

required init(promise: Promise,
path: URL,
enableShutterSound: Bool,
metadataProvider: MetadataProvider,
cameraSessionDelegate: CameraSessionDelegate?) {
self.promise = promise
self.path = path
self.enableShutterSound = enableShutterSound
self.metadataProvider = metadataProvider
self.cameraSessionDelegate = cameraSessionDelegate
Expand Down Expand Up @@ -48,7 +51,7 @@ class PhotoCaptureDelegate: GlobalReferenceHolder, AVCapturePhotoCaptureDelegate
}

do {
let path = try FileUtils.writePhotoToTempFile(photo: photo, metadataProvider: metadataProvider)
try FileUtils.writePhotoToFile(photo: photo, path: path, metadataProvider: metadataProvider)

let exif = photo.metadata["{Exif}"] as? [String: Any]
let width = exif?["PixelXDimension"]
Expand Down
5 changes: 5 additions & 0 deletions package/ios/Core/Types/RecordVideoOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Foundation

struct RecordVideoOptions {
var fileType: AVFileType = .mov
var path: String?
var flash: Torch = .off
var codec: AVVideoCodecType?
/**
Expand All @@ -28,6 +29,10 @@ struct RecordVideoOptions {
if let fileTypeOption = dictionary["fileType"] as? String {
fileType = try AVFileType(withString: fileTypeOption)
}
// Path
if let pathOption = dictionary["path"] as? String {
path = pathOption
}
// Flash
if let flashOption = dictionary["flash"] as? String {
flash = try Torch(jsValue: flashOption)
Expand Down
53 changes: 38 additions & 15 deletions package/ios/Core/Utils/FileUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,57 @@ import Foundation
import UIKit

enum FileUtils {
/**
Writes Data to a temporary file and returns the file path.
*/
private static func writeDataToTempFile(data: Data, fileExtension: String = "jpeg") throws -> URL {
let filename = UUID().uuidString + "." + fileExtension
let tempFilePath = FileManager.default.temporaryDirectory
.appendingPathComponent(filename)
private static func writeDataToFile(data: Data, path: URL) throws {
do {
try data.write(to: tempFilePath)
try data.write(to: path)
} catch {
throw CameraError.capture(.fileError(cause: error))
}
return tempFilePath
}

static func writePhotoToTempFile(photo: AVCapturePhoto, metadataProvider: MetadataProvider) throws -> URL {
private static func getTemporaryFileURL(fileExtension: String = "jpeg") -> URL {
let filename = UUID().uuidString + "." + fileExtension
return FileManager.default.temporaryDirectory.appendingPathComponent(filename)
}

static func writePhotoToFile(photo: AVCapturePhoto, path:URL, metadataProvider: MetadataProvider) throws {

Check failure on line 28 in package/ios/Core/Utils/FileUtils.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Colon Spacing Violation: Colons should be next to the identifier when specifying a type and next to the key in dictionary literals (colon)
guard let data = photo.fileDataRepresentation(with: metadataProvider) else {
throw CameraError.capture(.imageDataAccessError)
}
let path = try writeDataToTempFile(data: data)
return path
try writeDataToFile(data: data, path: path)
}

static func writeUIImageToTempFile(image: UIImage, compressionQuality: CGFloat = 1.0) throws -> URL {
static func writeUIImageToFile(image: UIImage, path: URL, compressionQuality: CGFloat = 1.0) throws {
guard let data = image.jpegData(compressionQuality: compressionQuality) else {
throw CameraError.capture(.imageDataAccessError)
}
let path = try writeDataToTempFile(data: data)
return path
try writeDataToFile(data: data, path: path)
}

static func getDestinationURL(path: String?, fileExtension: String) throws -> URL {
if let path = path {
let destinationURL = URL(string: path.starts(with: "file://") ? path : "file://" + path)!
let directory = destinationURL.deletingLastPathComponent()
// Check if the directory exists
if !FileManager.default.fileExists(atPath: directory.path) {
throw CameraError.capture(.invalidPath(message: "Directory does not exist: \(directory.path)"))
}
// Check if the directory is a directory
var isDirectory: ObjCBool = false
if !FileManager.default.fileExists(atPath: directory.path, isDirectory: &isDirectory) || !isDirectory.boolValue {
throw CameraError.capture(.invalidPath(message: "Path directory is not a directory: \(directory.path)"))
}
// Check if the directory is readable and writable
if !FileManager.default.isReadableFile(atPath: directory.path) || !FileManager.default.isWritableFile(atPath: directory.path) {
throw CameraError.capture(.invalidPath(message: "Path directory is not readable or writable: \(directory.path)"))
}
// Check if the path doesn't exist
if FileManager.default.fileExists(atPath: path) {
throw CameraError.capture(.invalidPath(message: "File already exists at path: \(path)"))
}
return destinationURL
} else {
return getTemporaryFileURL(fileExtension: fileExtension)
}
}
}
5 changes: 3 additions & 2 deletions package/ios/React/CameraView+TakeSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import AVFoundation
import UIKit

extension CameraView {
func takeSnapshot(options _: NSDictionary, promise: Promise) {
func takeSnapshot(options: NSDictionary, promise: Promise) {
// If video is not enabled, we won't get any buffers in onFrameListeners. abort it.
guard video else {
promise.reject(error: .capture(.videoNotEnabled))
Expand Down Expand Up @@ -40,7 +40,8 @@ extension CameraView {
let ciImage = CIImage(cvPixelBuffer: imageBuffer)
let image = UIImage(ciImage: ciImage, scale: 1.0, orientation: .up)
do {
let path = try FileUtils.writeUIImageToTempFile(image: image)
let path = try FileUtils.getDestinationURL(path: options["path"] as? String, fileExtension: "jpeg")
try FileUtils.writeUIImageToFile(image: image, path: path)
promise.resolve([
"path": path.absoluteString,
"width": image.size.width,
Expand Down
Loading

0 comments on commit d263742

Please sign in to comment.