From 7625c56e1e6a0a618757775f79f2611598aaa2b9 Mon Sep 17 00:00:00 2001 From: Rodrigo Fontes Date: Thu, 7 May 2026 12:14:15 -0500 Subject: [PATCH 1/6] feat: add support for specifying a video path --- .../hybrids/outputs/HybridVideoOutput.kt | 10 ++++++++-- .../Recording/HybridVideoRecorder.swift | 6 +++++- .../c++/JHybridCameraVideoOutputSpec.cpp | 1 + .../android/c++/JRecorderSettings.hpp | 7 ++++++- .../margelo/nitro/camera/RecorderSettings.kt | 9 +++++++-- .../c++/HybridCameraVideoOutputSpecSwift.hpp | 1 + .../ios/swift/RecorderSettings.swift | 20 ++++++++++++++++++- .../generated/shared/c++/RecorderSettings.hpp | 7 ++++++- .../specs/outputs/CameraVideoOutput.nitro.ts | 11 ++++++++-- 9 files changed, 62 insertions(+), 10 deletions(-) diff --git a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/outputs/HybridVideoOutput.kt b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/outputs/HybridVideoOutput.kt index ff442bf435..b3a28cccdd 100644 --- a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/outputs/HybridVideoOutput.kt +++ b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/outputs/HybridVideoOutput.kt @@ -145,8 +145,14 @@ class HybridVideoOutput( @SuppressLint("MissingPermission") override fun createRecorder(settings: RecorderSettings): Promise { return Promise.async { - // Create .mp4 file in temp directory - val file = File.createTempFile("VisionCamera_", "mp4") + val file = + if (settings.filePath != null) { + File(settings.filePath) + } else { + // Create .mp4 file in temp directory + File.createTempFile("VisionCamera_", "mp4") + } + file.parentFile?.mkdirs() // Prepare output options val fileOutputOptions = diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/HybridVideoRecorder.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/HybridVideoRecorder.swift index 21402327f7..e82e24ed6c 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/HybridVideoRecorder.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/HybridVideoRecorder.swift @@ -23,7 +23,11 @@ class HybridVideoRecorder: HybridRecorderSpec { ) throws { self.videoOutput = videoOutput self.queue = queue - self.fileURL = try URL.createTempURL(fileType: fileType.toUTType()) + if let filePath = settings.filePath { + self.fileURL = URL(fileURLWithPath: filePath) + } else { + self.fileURL = try URL.createTempURL(fileType: fileType.toUTType()) + } self.settings = settings super.init() } diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraVideoOutputSpec.cpp b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraVideoOutputSpec.cpp index 703c7f65e9..85af0b6759 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraVideoOutputSpec.cpp +++ b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JHybridCameraVideoOutputSpec.cpp @@ -38,6 +38,7 @@ namespace margelo::nitro::camera { enum class CameraOrientation; } #include "JRecorderSettings.hpp" #include "HybridLocationSpec.hpp" #include "JHybridLocationSpec.hpp" +#include #include "MediaType.hpp" #include "JMediaType.hpp" #include "CameraOrientation.hpp" diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JRecorderSettings.hpp b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JRecorderSettings.hpp index 8adfa642bb..627090a185 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/android/c++/JRecorderSettings.hpp +++ b/packages/react-native-vision-camera/nitrogen/generated/android/c++/JRecorderSettings.hpp @@ -14,6 +14,7 @@ #include "JHybridLocationSpec.hpp" #include #include +#include namespace margelo::nitro::camera { @@ -36,12 +37,15 @@ namespace margelo::nitro::camera { static const auto clazz = javaClassStatic(); static const auto fieldLocation = clazz->getField("location"); jni::local_ref location = this->getFieldValue(fieldLocation); + static const auto fieldFilePath = clazz->getField("filePath"); + jni::local_ref filePath = this->getFieldValue(fieldFilePath); static const auto fieldMaxDuration = clazz->getField("maxDuration"); jni::local_ref maxDuration = this->getFieldValue(fieldMaxDuration); static const auto fieldMaxFileSize = clazz->getField("maxFileSize"); jni::local_ref maxFileSize = this->getFieldValue(fieldMaxFileSize); return RecorderSettings( location != nullptr ? std::make_optional(location->getJHybridLocationSpec()) : std::nullopt, + filePath != nullptr ? std::make_optional(filePath->toStdString()) : std::nullopt, maxDuration != nullptr ? std::make_optional(maxDuration->value()) : std::nullopt, maxFileSize != nullptr ? std::make_optional(maxFileSize->value()) : std::nullopt ); @@ -53,12 +57,13 @@ namespace margelo::nitro::camera { */ [[maybe_unused]] static jni::local_ref fromCpp(const RecorderSettings& value) { - using JSignature = JRecorderSettings(jni::alias_ref, jni::alias_ref, jni::alias_ref); + using JSignature = JRecorderSettings(jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref); static const auto clazz = javaClassStatic(); static const auto create = clazz->getStaticMethod("fromCpp"); return create( clazz, value.location.has_value() ? std::dynamic_pointer_cast(value.location.value())->getJavaPart() : nullptr, + value.filePath.has_value() ? jni::make_jstring(value.filePath.value()) : nullptr, value.maxDuration.has_value() ? jni::JDouble::valueOf(value.maxDuration.value()) : nullptr, value.maxFileSize.has_value() ? jni::JDouble::valueOf(value.maxFileSize.value()) : nullptr ); diff --git a/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/RecorderSettings.kt b/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/RecorderSettings.kt index 6f193f8ef9..ed24d8fe35 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/RecorderSettings.kt +++ b/packages/react-native-vision-camera/nitrogen/generated/android/kotlin/com/margelo/nitro/camera/RecorderSettings.kt @@ -23,6 +23,9 @@ data class RecorderSettings( val location: HybridLocationSpec?, @DoNotStrip @Keep + val filePath: String?, + @DoNotStrip + @Keep val maxDuration: Double?, @DoNotStrip @Keep @@ -34,6 +37,7 @@ data class RecorderSettings( if (this === other) return true if (other !is RecorderSettings) return false return Objects.deepEquals(this.location, other.location) + && Objects.deepEquals(this.filePath, other.filePath) && Objects.deepEquals(this.maxDuration, other.maxDuration) && Objects.deepEquals(this.maxFileSize, other.maxFileSize) } @@ -41,6 +45,7 @@ data class RecorderSettings( override fun hashCode(): Int { return arrayOf( location, + filePath, maxDuration, maxFileSize ).contentDeepHashCode() @@ -54,8 +59,8 @@ data class RecorderSettings( @Keep @Suppress("unused") @JvmStatic - private fun fromCpp(location: HybridLocationSpec?, maxDuration: Double?, maxFileSize: Double?): RecorderSettings { - return RecorderSettings(location, maxDuration, maxFileSize) + private fun fromCpp(location: HybridLocationSpec?, filePath: String?, maxDuration: Double?, maxFileSize: Double?): RecorderSettings { + return RecorderSettings(location, filePath, maxDuration, maxFileSize) } } } diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraVideoOutputSpecSwift.hpp b/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraVideoOutputSpecSwift.hpp index 7f61857618..f993f0195f 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraVideoOutputSpecSwift.hpp +++ b/packages/react-native-vision-camera/nitrogen/generated/ios/c++/HybridCameraVideoOutputSpecSwift.hpp @@ -34,6 +34,7 @@ namespace margelo::nitro::camera { class HybridCameraOutputSpecSwift; } #include "HybridRecorderSpec.hpp" #include "RecorderSettings.hpp" #include "HybridLocationSpec.hpp" +#include #include "HybridCameraOutputSpecSwift.hpp" #include "VisionCamera-Swift-Cxx-Umbrella.hpp" diff --git a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/RecorderSettings.swift b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/RecorderSettings.swift index 7c5b1c6a68..e44feac27b 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/ios/swift/RecorderSettings.swift +++ b/packages/react-native-vision-camera/nitrogen/generated/ios/swift/RecorderSettings.swift @@ -18,7 +18,7 @@ public extension RecorderSettings { /** * Create a new instance of `RecorderSettings`. */ - init(location: (any HybridLocationSpec)?, maxDuration: Double?, maxFileSize: Double?) { + init(location: (any HybridLocationSpec)?, filePath: String?, maxDuration: Double?, maxFileSize: Double?) { self.init({ () -> bridge.std__optional_std__shared_ptr_HybridLocationSpec__ in if let __unwrappedValue = location { return bridge.create_std__optional_std__shared_ptr_HybridLocationSpec__({ () -> bridge.std__shared_ptr_HybridLocationSpec_ in @@ -28,6 +28,12 @@ public extension RecorderSettings { } else { return .init() } + }(), { () -> bridge.std__optional_std__string_ in + if let __unwrappedValue = filePath { + return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + } else { + return .init() + } }(), { () -> bridge.std__optional_double_ in if let __unwrappedValue = maxDuration { return bridge.create_std__optional_double_(__unwrappedValue) @@ -59,6 +65,18 @@ public extension RecorderSettings { }() } + @inline(__always) + var filePath: String? { + return { () -> String? in + if bridge.has_value_std__optional_std__string_(self.__filePath) { + let __unwrapped = bridge.get_std__optional_std__string_(self.__filePath) + return String(__unwrapped) + } else { + return nil + } + }() + } + @inline(__always) var maxDuration: Double? { return { () -> Double? in diff --git a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/RecorderSettings.hpp b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/RecorderSettings.hpp index be6d518e44..8a005cf217 100644 --- a/packages/react-native-vision-camera/nitrogen/generated/shared/c++/RecorderSettings.hpp +++ b/packages/react-native-vision-camera/nitrogen/generated/shared/c++/RecorderSettings.hpp @@ -34,6 +34,7 @@ namespace margelo::nitro::camera { class HybridLocationSpec; } #include #include "HybridLocationSpec.hpp" #include +#include namespace margelo::nitro::camera { @@ -43,12 +44,13 @@ namespace margelo::nitro::camera { struct RecorderSettings final { public: std::optional> location SWIFT_PRIVATE; + std::optional filePath SWIFT_PRIVATE; std::optional maxDuration SWIFT_PRIVATE; std::optional maxFileSize SWIFT_PRIVATE; public: RecorderSettings() = default; - explicit RecorderSettings(std::optional> location, std::optional maxDuration, std::optional maxFileSize): location(location), maxDuration(maxDuration), maxFileSize(maxFileSize) {} + explicit RecorderSettings(std::optional> location, std::optional filePath, std::optional maxDuration, std::optional maxFileSize): location(location), filePath(filePath), maxDuration(maxDuration), maxFileSize(maxFileSize) {} public: friend bool operator==(const RecorderSettings& lhs, const RecorderSettings& rhs) = default; @@ -65,6 +67,7 @@ namespace margelo::nitro { jsi::Object obj = arg.asObject(runtime); return margelo::nitro::camera::RecorderSettings( JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "location"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "filePath"))), JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "maxDuration"))), JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "maxFileSize"))) ); @@ -72,6 +75,7 @@ namespace margelo::nitro { static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::camera::RecorderSettings& arg) { jsi::Object obj(runtime); obj.setProperty(runtime, PropNameIDCache::get(runtime, "location"), JSIConverter>>::toJSI(runtime, arg.location)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "filePath"), JSIConverter>::toJSI(runtime, arg.filePath)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "maxDuration"), JSIConverter>::toJSI(runtime, arg.maxDuration)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "maxFileSize"), JSIConverter>::toJSI(runtime, arg.maxFileSize)); return obj; @@ -85,6 +89,7 @@ namespace margelo::nitro { return false; } if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "location")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "filePath")))) return false; if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "maxDuration")))) return false; if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "maxFileSize")))) return false; return true; diff --git a/packages/react-native-vision-camera/src/specs/outputs/CameraVideoOutput.nitro.ts b/packages/react-native-vision-camera/src/specs/outputs/CameraVideoOutput.nitro.ts index 32d882d303..870dbb71a3 100644 --- a/packages/react-native-vision-camera/src/specs/outputs/CameraVideoOutput.nitro.ts +++ b/packages/react-native-vision-camera/src/specs/outputs/CameraVideoOutput.nitro.ts @@ -143,6 +143,12 @@ export interface RecorderSettings { * into the video metadata using the ISO-6709 standard. */ location?: Location + /** + * The absolute path where the recording file should be written. + * + * If omitted, the recording will be written to a temporary file. + */ + filePath?: string; /** * If set, the recording automatically stops once it reaches * this duration, in seconds. @@ -210,8 +216,9 @@ export interface CameraVideoOutput extends CameraOutput { * Creates and prepares a new {@linkcode Recorder} * instance with the given {@linkcode RecorderSettings}. * - * The {@linkcode Recorder} will record to a temporary - * file, and can only record once. + * The {@linkcode Recorder} will record to the configured + * file path, or to a temporary file if no path was provided. + * It can only record once. * * If you want to create a second recording, * you must create a new {@linkcode Recorder}. From a494db9bee04ab0bb5bf14c9b5fc562f7766ca92 Mon Sep 17 00:00:00 2001 From: Rodrigo Fontes Date: Fri, 8 May 2026 08:35:03 -0500 Subject: [PATCH 2/6] fix: run linter --- .../src/specs/outputs/CameraVideoOutput.nitro.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-vision-camera/src/specs/outputs/CameraVideoOutput.nitro.ts b/packages/react-native-vision-camera/src/specs/outputs/CameraVideoOutput.nitro.ts index 870dbb71a3..5a1cbedfd0 100644 --- a/packages/react-native-vision-camera/src/specs/outputs/CameraVideoOutput.nitro.ts +++ b/packages/react-native-vision-camera/src/specs/outputs/CameraVideoOutput.nitro.ts @@ -148,7 +148,7 @@ export interface RecorderSettings { * * If omitted, the recording will be written to a temporary file. */ - filePath?: string; + filePath?: string /** * If set, the recording automatically stops once it reaches * this duration, in seconds. From 0327e0dd6ae43206fd2694420988d0d27311ad7f Mon Sep 17 00:00:00 2001 From: Rodrigo Fontes Date: Fri, 8 May 2026 10:13:02 -0500 Subject: [PATCH 3/6] fix: create parent dir on ios --- .../Hybrid Objects/Recording/HybridVideoRecorder.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/HybridVideoRecorder.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/HybridVideoRecorder.swift index e82e24ed6c..a3e04aa9e7 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/HybridVideoRecorder.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/HybridVideoRecorder.swift @@ -23,11 +23,15 @@ class HybridVideoRecorder: HybridRecorderSpec { ) throws { self.videoOutput = videoOutput self.queue = queue - if let filePath = settings.filePath { - self.fileURL = URL(fileURLWithPath: filePath) + if let customFilePath = settings.filePath { + self.fileURL = URL(fileURLWithPath: customFilePath) } else { self.fileURL = try URL.createTempURL(fileType: fileType.toUTType()) } + try FileManager.default.createDirectory( + at: self.fileURL.deletingLastPathComponent(), + withIntermediateDirectories: true + ) self.settings = settings super.init() } From 46ab94ede075221f6ff99d543a186b9a9850e1ea Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 8 May 2026 17:23:01 +0200 Subject: [PATCH 4/6] Update packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/outputs/HybridVideoOutput.kt --- .../margelo/nitro/camera/hybrids/outputs/HybridVideoOutput.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/outputs/HybridVideoOutput.kt b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/outputs/HybridVideoOutput.kt index b3a28cccdd..d39f2b8f33 100644 --- a/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/outputs/HybridVideoOutput.kt +++ b/packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/outputs/HybridVideoOutput.kt @@ -152,6 +152,7 @@ class HybridVideoOutput( // Create .mp4 file in temp directory File.createTempFile("VisionCamera_", "mp4") } + // Create all parent directories if they don't exist yet. file.parentFile?.mkdirs() // Prepare output options From 28435d9ad815f9c10a95572ecdbd5a074ca96b04 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 8 May 2026 17:23:10 +0200 Subject: [PATCH 5/6] Update packages/react-native-vision-camera/ios/Hybrid Objects/Recording/HybridVideoRecorder.swift --- .../ios/Hybrid Objects/Recording/HybridVideoRecorder.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/HybridVideoRecorder.swift b/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/HybridVideoRecorder.swift index a3e04aa9e7..b6280f6fbf 100644 --- a/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/HybridVideoRecorder.swift +++ b/packages/react-native-vision-camera/ios/Hybrid Objects/Recording/HybridVideoRecorder.swift @@ -28,6 +28,7 @@ class HybridVideoRecorder: HybridRecorderSpec { } else { self.fileURL = try URL.createTempURL(fileType: fileType.toUTType()) } + // Create all parent directories if they don't exist yet. try FileManager.default.createDirectory( at: self.fileURL.deletingLastPathComponent(), withIntermediateDirectories: true From c5af82f6e3e11828ff9270d307b9a0aa37955680 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Fri, 8 May 2026 17:23:17 +0200 Subject: [PATCH 6/6] Update packages/react-native-vision-camera/src/specs/outputs/CameraVideoOutput.nitro.ts --- .../src/specs/outputs/CameraVideoOutput.nitro.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/react-native-vision-camera/src/specs/outputs/CameraVideoOutput.nitro.ts b/packages/react-native-vision-camera/src/specs/outputs/CameraVideoOutput.nitro.ts index 5a1cbedfd0..d6fd9551fc 100644 --- a/packages/react-native-vision-camera/src/specs/outputs/CameraVideoOutput.nitro.ts +++ b/packages/react-native-vision-camera/src/specs/outputs/CameraVideoOutput.nitro.ts @@ -144,9 +144,14 @@ export interface RecorderSettings { */ location?: Location /** - * The absolute path where the recording file should be written. - * - * If omitted, the recording will be written to a temporary file. + * The absolute path (including file name and extension) where + * the recording file should be written to, or `undefined` to + * create a file in the device's temporary directory. + * + * All parent directories in this {@linkcode filePath} will + * be automatically created if they do not yet exist. + * + * @default undefined */ filePath?: string /**