Compare commits

..

14 Commits

Author SHA1 Message Date
61863149c0 flip orientation in camera session 2025-12-11 14:17:34 -08:00
09b50938d2 get orientation change from WindowManager for android 2025-12-11 13:02:00 -08:00
a158ed8350 Merge pull request 'Bump react native w/ api 35 compatability' (#10) from bump-react-native-with-api35 into main
Reviewed-on: #10
2025-12-11 18:04:51 +00:00
Dean
e7b295546a fix: Add null safety checks in ChunkedRecordingManager
Replace !! operators with proper null checks to prevent
NullPointerExceptions when encodedFormat or muxerContext are null.
This can happen if createNextMuxer is called before
onOutputFormatChanged sets the format.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 10:04:18 -08:00
Dean
d87ed8ced2 fix: Handle null error message in promise rejection
Prevents crash when an exception with null message is caught and rejected
through the React Native bridge.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 09:48:32 -08:00
f055119735 respect frame processor flag when compiling and force 16kb page alignment 2025-12-01 11:24:24 -07:00
35d80b13d6 disable frame processor jni bindings, preserve video pipeline registration 2025-11-24 10:30:33 -08:00
3d09106e45 skip native library loading for frame processors, wip: failing to launch app 2025-11-17 18:58:13 -08:00
b523e1884f Fix React Native 0.79 Kotlin compilation errors
- Fix currentActivity reference in CameraViewModule
- Convert Map to MutableMap in CameraViewManager for RN 0.79 compatibility
2025-11-17 13:38:30 -08:00
5fcc1a4f77 Bumps and fixes for react native version bump 2025-11-17 13:35:59 -08:00
364171a107 Update Java/Kotlin versions and add opt-in flags for RN 0.79 2025-11-17 13:35:25 -08:00
f90e11897f Fix CMake target for React Native 0.79 compatibility 2025-11-17 11:45:07 -08:00
4798aad464 Merge pull request 'fix/android-api-35-bitmap-config' (#9) from fix/android-api-35-bitmap-config into main
Reviewed-on: #9
2025-10-29 03:22:49 +00:00
Dean
2c8d503e66 Fix Bitmap.Config null-safety for Android API 35 2025-10-28 13:54:54 -07:00
11 changed files with 121 additions and 47 deletions

View File

@@ -19,9 +19,7 @@ endif()
# Add react-native-vision-camera sources # Add react-native-vision-camera sources
add_library( set(SOURCES
${PACKAGE_NAME}
SHARED
# Shared C++ # Shared C++
../cpp/MutableRawBuffer.cpp ../cpp/MutableRawBuffer.cpp
# Java JNI # Java JNI
@@ -31,17 +29,33 @@ add_library(
src/main/cpp/OpenGLContext.cpp src/main/cpp/OpenGLContext.cpp
src/main/cpp/OpenGLRenderer.cpp src/main/cpp/OpenGLRenderer.cpp
src/main/cpp/MutableJByteBuffer.cpp src/main/cpp/MutableJByteBuffer.cpp
# Frame Processor )
src/main/cpp/frameprocessor/FrameHostObject.cpp
src/main/cpp/frameprocessor/FrameProcessorPluginHostObject.cpp # Only add Frame Processor sources if enabled
src/main/cpp/frameprocessor/JSIJNIConversion.cpp if (ENABLE_FRAME_PROCESSORS)
src/main/cpp/frameprocessor/VisionCameraProxy.cpp list(APPEND SOURCES
src/main/cpp/frameprocessor/java-bindings/JSharedArray.cpp src/main/cpp/frameprocessor/FrameHostObject.cpp
src/main/cpp/frameprocessor/java-bindings/JFrame.cpp src/main/cpp/frameprocessor/FrameProcessorPluginHostObject.cpp
src/main/cpp/frameprocessor/java-bindings/JFrameProcessor.cpp src/main/cpp/frameprocessor/JSIJNIConversion.cpp
src/main/cpp/frameprocessor/java-bindings/JFrameProcessorPlugin.cpp src/main/cpp/frameprocessor/VisionCameraProxy.cpp
src/main/cpp/frameprocessor/java-bindings/JVisionCameraProxy.cpp src/main/cpp/frameprocessor/java-bindings/JSharedArray.cpp
src/main/cpp/frameprocessor/java-bindings/JVisionCameraScheduler.cpp src/main/cpp/frameprocessor/java-bindings/JFrame.cpp
src/main/cpp/frameprocessor/java-bindings/JFrameProcessor.cpp
src/main/cpp/frameprocessor/java-bindings/JFrameProcessorPlugin.cpp
src/main/cpp/frameprocessor/java-bindings/JVisionCameraProxy.cpp
src/main/cpp/frameprocessor/java-bindings/JVisionCameraScheduler.cpp
)
endif()
add_library(
${PACKAGE_NAME}
SHARED
${SOURCES}
)
# Force 16KB page alignment for Android 15+ compatibility
set_target_properties(${PACKAGE_NAME} PROPERTIES
LINK_FLAGS "-Wl,-z,max-page-size=16384"
) )
# Header Search Paths (includes) # Header Search Paths (includes)
@@ -60,13 +74,13 @@ target_include_directories(
# Link everything together # Link everything together
target_link_libraries( target_link_libraries(
${PACKAGE_NAME} ${PACKAGE_NAME}
${LOG_LIB} # <-- Logcat logger ${LOG_LIB} # <-- Logcat logger
android # <-- Android JNI core android # <-- Android JNI core
ReactAndroid::jsi # <-- RN: JSI ReactAndroid::jsi # <-- RN: JSI
# ReactAndroid::reactnativejni # <-- Temporarily disabled for RN 0.79+ compatibility ReactAndroid::reactnative # <-- RN: React Native JNI bindings (RN 0.76+)
fbjni::fbjni # <-- fbjni fbjni::fbjni # <-- fbjni
GLESv2 # <-- OpenGL (for VideoPipeline) GLESv2 # <-- OpenGL (for VideoPipeline)
EGL # <-- OpenGL (EGL) (for VideoPipeline) EGL # <-- OpenGL (EGL) (for VideoPipeline)
) )
# Optionally also add Frame Processors here # Optionally also add Frame Processors here

View File

@@ -133,8 +133,16 @@ android {
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
freeCompilerArgs += [
"-opt-in=kotlin.RequiresOptIn",
"-opt-in=com.facebook.react.annotations.UnstableReactNativeAPI"
]
} }
externalNativeBuild { externalNativeBuild {
@@ -157,6 +165,7 @@ android {
"**/libhermes-executor-debug.so", "**/libhermes-executor-debug.so",
"**/libhermes_executor.so", "**/libhermes_executor.so",
"**/libreactnativejni.so", "**/libreactnativejni.so",
"**/libreactnative.so",
"**/libturbomodulejsijni.so", "**/libturbomodulejsijni.so",
"**/libreact_nativemodule_core.so", "**/libreact_nativemodule_core.so",
"**/libjscexecutor.so" "**/libjscexecutor.so"

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -9,11 +9,13 @@
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
return facebook::jni::initialize(vm, [] { return facebook::jni::initialize(vm, [] {
// VideoPipeline is needed for video recording even without Frame Processors
vision::VideoPipeline::registerNatives();
#if VISION_CAMERA_ENABLE_FRAME_PROCESSORS
// Frame Processor JNI bindings - only register when Frame Processors are enabled
vision::VisionCameraInstaller::registerNatives(); vision::VisionCameraInstaller::registerNatives();
vision::JVisionCameraProxy::registerNatives(); vision::JVisionCameraProxy::registerNatives();
vision::JVisionCameraScheduler::registerNatives(); vision::JVisionCameraScheduler::registerNatives();
vision::VideoPipeline::registerNatives();
#if VISION_CAMERA_ENABLE_FRAME_PROCESSORS
vision::JFrameProcessor::registerNatives(); vision::JFrameProcessor::registerNatives();
vision::JSharedArray::registerNatives(); vision::JSharedArray::registerNatives();
#endif #endif

View File

@@ -32,7 +32,7 @@ class CameraViewManager : ViewGroupManager<CameraView>() {
.put("cameraError", MapBuilder.of("registrationName", "onError")) .put("cameraError", MapBuilder.of("registrationName", "onError"))
.put("cameraCodeScanned", MapBuilder.of("registrationName", "onCodeScanned")) .put("cameraCodeScanned", MapBuilder.of("registrationName", "onCodeScanned"))
.put("onVideoChunkReady", MapBuilder.of("registrationName", "onVideoChunkReady")) .put("onVideoChunkReady", MapBuilder.of("registrationName", "onVideoChunkReady"))
.build() .build()?.toMutableMap()
override fun getName(): String = TAG override fun getName(): String = TAG

View File

@@ -31,10 +31,12 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
init { init {
try { try {
// Load the native part of VisionCamera. // Load the native part of VisionCamera.
// Includes the OpenGL VideoPipeline, as well as Frame Processor JSI bindings // Includes the OpenGL VideoPipeline (needed for video recording)
// Frame Processors remain disabled for RN 0.79+ compatibility
System.loadLibrary("VisionCamera") System.loadLibrary("VisionCamera")
Log.i(TAG, "VisionCamera native library loaded successfully")
} catch (e: UnsatisfiedLinkError) { } catch (e: UnsatisfiedLinkError) {
Log.e(VisionCameraProxy.TAG, "Failed to load VisionCamera C++ library!", e) Log.e(TAG, "Failed to load VisionCamera C++ library!", e)
throw e throw e
} }
} }
@@ -73,15 +75,11 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
} }
@ReactMethod(isBlockingSynchronousMethod = true) @ReactMethod(isBlockingSynchronousMethod = true)
fun installFrameProcessorBindings(): Boolean = fun installFrameProcessorBindings(): Boolean {
try { // Frame Processors are disabled for React Native 0.79+ compatibility
val proxy = VisionCameraProxy(reactApplicationContext) Log.i(TAG, "Frame Processor bindings not installed - Frame Processors disabled for RN 0.79+ compatibility")
VisionCameraInstaller.install(proxy) return false
true }
} catch (e: Error) {
Log.e(TAG, "Failed to install Frame Processor JSI Bindings!", e)
false
}
@ReactMethod @ReactMethod
fun takePhoto(viewTag: Int, options: ReadableMap, promise: Promise) { fun takePhoto(viewTag: Int, options: ReadableMap, promise: Promise) {
@@ -157,7 +155,7 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
} }
private fun canRequestPermission(permission: String): Boolean { private fun canRequestPermission(permission: String): Boolean {
val activity = currentActivity as? PermissionAwareActivity val activity = reactApplicationContext.currentActivity as? PermissionAwareActivity
return activity?.shouldShowRequestPermissionRationale(permission) ?: false return activity?.shouldShowRequestPermissionRationale(permission) ?: false
} }

View File

@@ -15,6 +15,7 @@ import android.util.Log
import android.util.Size import android.util.Size
import android.view.Surface import android.view.Surface
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.WindowManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.google.mlkit.vision.barcode.common.Barcode import com.google.mlkit.vision.barcode.common.Barcode
import com.mrousavy.camera.core.capture.RepeatingCaptureRequest import com.mrousavy.camera.core.capture.RepeatingCaptureRequest
@@ -425,6 +426,21 @@ class CameraSession(private val context: Context, private val cameraManager: Cam
val fps = configuration?.fps ?: 30 val fps = configuration?.fps ?: 30
// Get actual device rotation from WindowManager since the React Native orientation hook
// doesn't update when rotating between landscape-left and landscape-right on Android.
// Map device rotation to the correct orientationHint for video recording:
// - Counter-clockwise (ROTATION_90) → 270° hint
// - Clockwise (ROTATION_270) → 90° hint
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val deviceRotation = windowManager.defaultDisplay.rotation
val recordingOrientation = when (deviceRotation) {
Surface.ROTATION_0 -> Orientation.PORTRAIT
Surface.ROTATION_90 -> Orientation.LANDSCAPE_RIGHT
Surface.ROTATION_180 -> Orientation.PORTRAIT_UPSIDE_DOWN
Surface.ROTATION_270 -> Orientation.LANDSCAPE_LEFT
else -> Orientation.PORTRAIT
}
val recording = RecordingSession( val recording = RecordingSession(
context, context,
cameraId, cameraId,

View File

@@ -105,6 +105,12 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
muxerContext?.finish() muxerContext?.finish()
chunkIndex++ chunkIndex++
val format = this.encodedFormat
if (format == null) {
Log.e(TAG, "Cannot create muxer: encodedFormat is null (onOutputFormatChanged not called yet)")
return
}
val newFileName = "$chunkIndex.mp4" val newFileName = "$chunkIndex.mp4"
val newOutputFile = File(this.outputDirectory, newFileName) val newOutputFile = File(this.outputDirectory, newFileName)
Log.i(TAG, "Creating new muxer for file: $newFileName") Log.i(TAG, "Creating new muxer for file: $newFileName")
@@ -114,7 +120,7 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
) )
muxer.setOrientationHint(orientationHint) muxer.setOrientationHint(orientationHint)
muxerContext = MuxerContext( muxerContext = MuxerContext(
muxer, newOutputFile, chunkIndex, bufferInfo.presentationTimeUs, this.encodedFormat!!, this.callbacks muxer, newOutputFile, chunkIndex, bufferInfo.presentationTimeUs, format, this.callbacks
) )
} }
@@ -123,7 +129,8 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
} }
private fun chunkLengthUs(bufferInfo: BufferInfo): Long { private fun chunkLengthUs(bufferInfo: BufferInfo): Long {
return bufferInfo.presentationTimeUs - muxerContext!!.startTimeUs val context = muxerContext ?: return 0L
return bufferInfo.presentationTimeUs - context.startTimeUs
} }
fun start() { fun start() {
@@ -155,7 +162,13 @@ class ChunkedRecordingManager(private val encoder: MediaCodec, private val outpu
if (muxerContext == null || (atKeyframe(bufferInfo) && chunkLengthUs(bufferInfo) >= targetDurationUs)) { if (muxerContext == null || (atKeyframe(bufferInfo) && chunkLengthUs(bufferInfo) >= targetDurationUs)) {
this.createNextMuxer(bufferInfo) this.createNextMuxer(bufferInfo)
} }
muxerContext!!.muxer.writeSampleData(muxerContext!!.videoTrack, encodedData, bufferInfo) val context = muxerContext
if (context == null) {
Log.e(TAG, "Cannot write sample data: muxerContext is null")
encoder.releaseOutputBuffer(index, false)
return
}
context.muxer.writeSampleData(context.videoTrack, encodedData, bufferInfo)
encoder.releaseOutputBuffer(index, false) encoder.releaseOutputBuffer(index, false)
} }
} }

View File

@@ -9,8 +9,10 @@ import android.os.Looper
import android.util.Log import android.util.Log
import android.util.Size import android.util.Size
import android.view.PixelCopy import android.view.PixelCopy
import android.view.Surface
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.SurfaceView import android.view.SurfaceView
import android.view.WindowManager
import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.bridge.UiThreadUtil
import com.mrousavy.camera.extensions.resize import com.mrousavy.camera.extensions.resize
import com.mrousavy.camera.extensions.rotatedBy import com.mrousavy.camera.extensions.rotatedBy
@@ -150,6 +152,8 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) :
val width = frame.width() val width = frame.width()
val height = frame.height() val height = frame.height()
// Create bitmap matching surface frame dimensions for PixelCopy
// The original code swapped dimensions assuming landscape input - keep that for consistency
val bitmap = Bitmap.createBitmap(height, width, Bitmap.Config.ARGB_8888) val bitmap = Bitmap.createBitmap(height, width, Bitmap.Config.ARGB_8888)
// Use a coroutine to suspend until the PixelCopy request is complete // Use a coroutine to suspend until the PixelCopy request is complete
@@ -159,7 +163,23 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) :
bitmap, bitmap,
{ copyResult -> { copyResult ->
if (copyResult == PixelCopy.SUCCESS) { if (copyResult == PixelCopy.SUCCESS) {
continuation.resume(rotateBitmap90CounterClockwise(bitmap)) // Get actual device rotation from WindowManager instead of relying on
// the orientation prop, which may not update on Android when rotating
// between landscape-left and landscape-right.
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val deviceRotation = windowManager.defaultDisplay.rotation
val actualOrientation = when (deviceRotation) {
Surface.ROTATION_0 -> Orientation.PORTRAIT
Surface.ROTATION_90 -> Orientation.LANDSCAPE_LEFT
Surface.ROTATION_180 -> Orientation.PORTRAIT_UPSIDE_DOWN
Surface.ROTATION_270 -> Orientation.LANDSCAPE_RIGHT
else -> Orientation.PORTRAIT
}
Log.i(TAG, "getBitmap: orientation prop = $orientation, deviceRotation = $deviceRotation, actualOrientation = $actualOrientation")
continuation.resume(bitmap.transformBitmap(actualOrientation))
} else { } else {
continuation.resumeWithException( continuation.resumeWithException(
RuntimeException("PixelCopy failed with error code $copyResult") RuntimeException("PixelCopy failed with error code $copyResult")

View File

@@ -9,7 +9,6 @@ import com.facebook.jni.HybridData
import com.facebook.proguard.annotations.DoNotStrip import com.facebook.proguard.annotations.DoNotStrip
import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.bridge.UiThreadUtil
// import com.facebook.react.turbomodule.core.CallInvokerHolderImpl // Commented out due to RN 0.79+ compatibility
import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.UIManagerHelper
import com.mrousavy.camera.CameraView import com.mrousavy.camera.CameraView
import com.mrousavy.camera.core.ViewNotFoundError import com.mrousavy.camera.core.ViewNotFoundError
@@ -78,6 +77,9 @@ class VisionCameraProxy(private val reactContext: ReactApplicationContext) {
FrameProcessorPluginRegistry.getPlugin(name, this, options) FrameProcessorPluginRegistry.getPlugin(name, this, options)
// private C++ funcs // private C++ funcs
// Commented out due to React Native 0.79+ API compatibility issues // Frame Processors are disabled - native registration is skipped via VISION_CAMERA_ENABLE_FRAME_PROCESSORS=OFF
// private external fun initHybrid(jsContext: Long, jsCallInvokerHolder: CallInvokerHolderImpl, scheduler: VisionCameraScheduler): HybridData // This method is never called or registered, kept for reference only
// @DoNotStrip
// @Keep
// private external fun initHybrid(jsContext: Long, jsCallInvokerHolder: Any, scheduler: VisionCameraScheduler): HybridData
} }

View File

@@ -11,6 +11,6 @@ inline fun withPromise(promise: Promise, closure: () -> Any?) {
} catch (e: Throwable) { } catch (e: Throwable) {
e.printStackTrace() e.printStackTrace()
val error = if (e is CameraError) e else UnknownCameraError(e) val error = if (e is CameraError) e else UnknownCameraError(e)
promise.reject("${error.domain}/${error.id}", error.message, error.cause) promise.reject("${error.domain}/${error.id}", error.message ?: "Unknown error", error.cause)
} }
} }