Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# CHANGELOG

## Unreleased

### Added

- **`VROARSession::stopRecording` takes a completion receiving a `VROARRecordingResult`** (`success`, `error`, `durationSec`, `frames`, `depthFrames`, `gpsFixes`, `bytes`), called once the files are complete. The no-argument form remains. On Android, `ARScene.stopRecording()` returns an `ARScene.RecordingResult` with the same fields.
- **`VROARSession::isRecordingDepthSupported()` and `ARScene.isRecordingSupported(boolean depth)`.** iOS checks for LiDAR scene depth and ARCore for the Depth API. On Android `ARScene.isRecordingSupported` answers false, with or without depth, until the ARCore session exists.

### Fixed

- **Android reports a recording whose encoder did not finish as failed.** It logged a warning and the recording counted as complete, while iOS reports the same case as an error.
- **Android: a failure while a recording is being stopped no longer leaves `getRecordingStatus()` at IOError (`ARSessionRecorder`).** `encoder.stop()` and `muxer.stop()` failures reported it to the scene after the stop had already set the status back to None, so the status read IOError with nothing recording. Such a failure is now reported in the stop's result only. `ARScene.stopRecording()` may be called off the UI thread.
- **Android hit tests always deliver their results (`VRORendererARCore_JNI`, `VRORenderer_JNI`).** The AR point, ray and position hit tests, the Quest OpenXR hit tests and the scene hit test kept the caller's one-shot listener only as a weak global reference, so the garbage collector could take it before the renderer thread answered, and the results never arrived. It is now a global reference, deleted once the callback has run.

## v3.1.0 — 27 October 2026

### Added
Expand Down
41 changes: 38 additions & 3 deletions ViroRenderer/VROARSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#ifndef VROARSession_h
#define VROARSession_h

#include <cstdint>
#include <functional>
#include <memory>
#include <set>
Expand Down Expand Up @@ -148,6 +149,20 @@ struct VROARRecordingConfig {
bool depth = false;
};

/*
What a finished recording wrote, passed to stopRecording()'s callback once
video.mp4, session.jsonl and any depth files are complete.
*/
struct VROARRecordingResult {
bool success = false;
std::string error; // Empty on success
double durationSec = 0; // First to last recorded frame
int frames = 0; // Camera frames, one `pose` line each
int depthFrames = 0; // `depth` lines (PNG pairs) written
int gpsFixes = 0; // `pose` lines carrying a `gps` object
int64_t bytes = -1; // Size of every file written; < 0 when unknown
};

/*
Manages the device camera and motion tracking for AR.
*/
Expand Down Expand Up @@ -981,6 +996,14 @@ class VROARSession {
return false;
}

/*
Returns true if this session can also record depth (see
VROARRecordingConfig::depth): LiDAR on iOS, the Depth API on ARCore.
*/
virtual bool isRecordingDepthSupported() const {
return false;
}

/*
Start recording video + IMU + ground-truth pose/anchors to config.outputDir.
onSuccess is invoked once recording has actually started (files created,
Expand All @@ -996,10 +1019,22 @@ class VROARSession {
}

/*
Stop recording and finalize video.mp4 + session.jsonl. No-op if not
currently recording.
Stop recording and finalize video.mp4 + session.jsonl. onStopped is invoked
once the files are complete, with success false (error "Not recording")
if no recording was running. May block the calling thread while the
writers finish.
*/
virtual void stopRecording() {}
virtual void stopRecording(std::function<void(const VROARRecordingResult &result)> onStopped) {
if (onStopped) {
VROARRecordingResult result;
result.error = "Recording not supported";
onStopped(result);
}
}

void stopRecording() {
stopRecording(nullptr);
}

/*
The current recording state. See VROARRecordingStatus.
Expand Down
16 changes: 16 additions & 0 deletions android/sharedCode/src/main/cpp/arcore/ARSceneController_JNI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1971,6 +1971,22 @@ VRO_METHOD(VRO_INT, nativeGetRecordingStatus)(VRO_ARGS
return (VRO_INT) session->getRecordingStatus();
}

VRO_METHOD(VRO_BOOL, nativeIsRecordingSupported)(VRO_ARGS
VRO_REF(VROARSceneController) sceneController_j,
VRO_BOOL depth) {
VRO_METHOD_PREAMBLE;
std::shared_ptr<VROARScene> scene = std::dynamic_pointer_cast<VROARScene>(
VRO_REF_GET(VROARSceneController, sceneController_j)->getScene());
if (!scene) {
return false;
}
std::shared_ptr<VROARSession> session = scene->getARSession();
if (!session || !session->isRecordingSupported()) {
return false;
}
return !depth || session->isRecordingDepthSupported();
}

// Called by Java (ARSessionRecorder, via ARScene) when the encoder/sidecar
// hits an IOException mid-recording — flips native's getRecordingStatus() to
// IOError so the JS layer can surface it instead of silently losing frames.
Expand Down
17 changes: 16 additions & 1 deletion android/sharedCode/src/main/cpp/arcore/VROARSessionARCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3149,10 +3149,25 @@ void VROARSessionARCore::startRecording(const VROARRecordingConfig &config,
}
}

void VROARSessionARCore::stopRecording() {
bool VROARSessionARCore::isRecordingDepthSupported() const {
return _session != nullptr && _session->isDepthModeSupported(arcore::DepthMode::Automatic);
}

void VROARSessionARCore::stopRecording(std::function<void(const VROARRecordingResult &result)> onStopped) {
VROARRecordingStatus status = _recordingStatus;
_recordingStatus = VROARRecordingStatus::None;
_recordingDepth = false;
setRecordingJavaCallback(nullptr);
if (onStopped) {
VROARRecordingResult result;
if (status == VROARRecordingStatus::Recording) {
result.success = true;
} else {
result.error = status == VROARRecordingStatus::IOError ? "Recording stopped after a write error"
: "Not recording";
}
onStopped(result);
}
}

VROARRecordingStatus VROARSessionARCore::getRecordingStatus() const {
Expand Down
8 changes: 7 additions & 1 deletion android/sharedCode/src/main/cpp/arcore/VROARSessionARCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -305,10 +305,16 @@ class VROARSessionARCore : public VROARSession,
recordFrameForRecording() and setRecordingJavaCallback() below.
*/
bool isRecordingSupported() const override { return true; }
bool isRecordingDepthSupported() const override;
void startRecording(const VROARRecordingConfig &config,
std::function<void()> onSuccess,
std::function<void(std::string error)> onFailure) override;
void stopRecording() override;
/*
The files are written in Java, so the result passed here carries only
success; ARScene.stopRecording() returns the counts and sizes.
*/
using VROARSession::stopRecording;
void stopRecording(std::function<void(const VROARRecordingResult &result)> onStopped) override;
VROARRecordingStatus getRecordingStatus() const override;

/*
Expand Down
66 changes: 33 additions & 33 deletions android/sharedCode/src/main/cpp/jni/VRORendererARCore_JNI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,17 @@ VRO_METHOD(void, nativeSetAnchorDetectionTypes)(VRO_ARGS

}

void invokeEmptyARResultsCallback(jweak weakCallback);
void invokeEmptyARResultsCallback(jobject callbackRef);

void invokeARResultsCallback(std::vector<std::shared_ptr<VROARHitTestResult>> &results, jweak weakCallback) {
// callbackRef is a global ref, deleted once the callback runs. Callers pass a one-shot listener
// nothing else holds, so a weak ref let the GC collect it first and the result never arrived.
void invokeARResultsCallback(std::vector<std::shared_ptr<VROARHitTestResult>> &results, jobject callbackRef) {
JNIEnv *env = VROPlatformGetJNIEnv();
// Runs on the renderer thread, which on Quest is a native OpenXR thread: plain FindClass
// does not see app classes there, and the pending ClassNotFoundException aborted the app.
jclass arHitTestResultClass = VROPlatformFindHostClass(env, "com/viro/core/ARHitTestResult");
if (arHitTestResultClass == nullptr) {
invokeEmptyARResultsCallback(weakCallback);
invokeEmptyARResultsCallback(callbackRef);
return;
}

Expand All @@ -137,62 +139,60 @@ void invokeARResultsCallback(std::vector<std::shared_ptr<VROARHitTestResult>> &r
jobject globalArrayRef = env->NewGlobalRef(resultsArray);
env->DeleteLocalRef(resultsArray);
env->DeleteLocalRef(arHitTestResultClass);
VROPlatformDispatchAsyncApplication([weakCallback, globalArrayRef] {
VROPlatformDispatchAsyncApplication([callbackRef, globalArrayRef] {
JNIEnv *env = VROPlatformGetJNIEnv();
jobject callback = env->NewLocalRef(weakCallback);
VROPlatformCallHostFunction(callback, "onHitTestFinished",
VROPlatformCallHostFunction(callbackRef, "onHitTestFinished",
"([Lcom/viro/core/ARHitTestResult;)V",
globalArrayRef);
env->DeleteGlobalRef(globalArrayRef);
env->DeleteWeakGlobalRef(weakCallback);
env->DeleteGlobalRef(callbackRef);
});
}

void invokeEmptyARResultsCallback(jweak weakCallback) {
VROPlatformDispatchAsyncApplication([weakCallback] {
void invokeEmptyARResultsCallback(jobject callbackRef) {
VROPlatformDispatchAsyncApplication([callbackRef] {
JNIEnv *env = VROPlatformGetJNIEnv();
jobject callback = env->NewLocalRef(weakCallback);
jclass arHitTestResultClass = env->FindClass("com/viro/core/ARHitTestResult");
jobjectArray emptyArray = env->NewObjectArray(0, arHitTestResultClass, NULL);
VROPlatformCallHostFunction(callback, "onHitTestFinished",
VROPlatformCallHostFunction(callbackRef, "onHitTestFinished",
"([Lcom/viro/core/ARHitTestResult;)V", emptyArray);
env->DeleteWeakGlobalRef(weakCallback);
env->DeleteGlobalRef(callbackRef);
});
}

void performARHitTest(VROVector3f rayVec, std::weak_ptr<VROSceneRendererARCore> arRenderer_w,
jweak weakCallback) {
jobject callbackRef) {
std::shared_ptr<VROSceneRendererARCore> arRenderer = arRenderer_w.lock();
if (!arRenderer) {
invokeEmptyARResultsCallback(weakCallback);
invokeEmptyARResultsCallback(callbackRef);
}
else {
std::vector<std::shared_ptr<VROARHitTestResult>> results = arRenderer->performARHitTest(rayVec);
invokeARResultsCallback(results, weakCallback);
invokeARResultsCallback(results, callbackRef);
}
}

void performARHitTestRay(VROVector3f rayOrigin, VROVector3f rayDestination, std::weak_ptr<VROSceneRendererARCore> arRenderer_w,
jweak weakCallback) {
jobject callbackRef) {
std::shared_ptr<VROSceneRendererARCore> arRenderer = arRenderer_w.lock();
if (!arRenderer) {
invokeEmptyARResultsCallback(weakCallback);
invokeEmptyARResultsCallback(callbackRef);
}
else {
std::vector<std::shared_ptr<VROARHitTestResult>> results = arRenderer->performARHitTest(rayOrigin, rayDestination);
invokeARResultsCallback(results, weakCallback);
invokeARResultsCallback(results, callbackRef);
}
}

void performARHitTestPoint(JNIEnv *env, float x, float y, std::weak_ptr<VROSceneRendererARCore> arRenderer_w,
jweak weakCallback) {
jobject callbackRef) {
std::shared_ptr<VROSceneRendererARCore> arRenderer = arRenderer_w.lock();
if (!arRenderer) {
invokeEmptyARResultsCallback(weakCallback);
invokeEmptyARResultsCallback(callbackRef);
}
else {
std::vector<std::shared_ptr<VROARHitTestResult>> results = arRenderer->performARHitTest(x, y);
invokeARResultsCallback(results, weakCallback);
invokeARResultsCallback(results, callbackRef);
}
}

Expand All @@ -208,10 +208,10 @@ VRO_METHOD(void, nativePerformARHitTestWithRay) (VRO_ARGS
// Create weak pointers for dispatching
std::shared_ptr<VROSceneRenderer> renderer = Renderer::native(native_renderer);
std::weak_ptr<VROSceneRendererARCore> arRenderer_w = std::dynamic_pointer_cast<VROSceneRendererARCore>(renderer);
jweak weakCallback = env->NewWeakGlobalRef(callback);
jobject callbackRef = env->NewGlobalRef(callback);

VROPlatformDispatchAsyncRenderer([arRenderer_w, weakCallback, rayVec] {
performARHitTest(rayVec, arRenderer_w, weakCallback);
VROPlatformDispatchAsyncRenderer([arRenderer_w, callbackRef, rayVec] {
performARHitTest(rayVec, arRenderer_w, callbackRef);
});
}

Expand All @@ -233,10 +233,10 @@ VRO_METHOD(void, nativePerformARHitTestWithOriginDestRay) (VRO_ARGS
// Create weak pointers for dispatching
std::shared_ptr<VROSceneRenderer> renderer = Renderer::native(native_renderer);
std::weak_ptr<VROSceneRendererARCore> arRenderer_w = std::dynamic_pointer_cast<VROSceneRendererARCore>(renderer);
jweak weakCallback = env->NewWeakGlobalRef(callback);
jobject callbackRef = env->NewGlobalRef(callback);

VROPlatformDispatchAsyncRenderer([arRenderer_w, weakCallback, rayOrigVec, rayDestVec] {
performARHitTestRay(rayOrigVec, rayDestVec, arRenderer_w, weakCallback);
VROPlatformDispatchAsyncRenderer([arRenderer_w, callbackRef, rayOrigVec, rayDestVec] {
performARHitTestRay(rayOrigVec, rayDestVec, arRenderer_w, callbackRef);
});
}

Expand All @@ -257,10 +257,10 @@ VRO_METHOD(void, nativePerformARHitTestWithPosition) (VRO_ARGS

// Create weak pointers for dispatching
std::weak_ptr<VROSceneRendererARCore> arRenderer_w = std::dynamic_pointer_cast<VROSceneRendererARCore>(renderer);
jweak weakCallback = env->NewWeakGlobalRef(callback);
jobject callbackRef = env->NewGlobalRef(callback);

VROPlatformDispatchAsyncRenderer([arRenderer_w, weakCallback, rayVec] {
performARHitTest(rayVec, arRenderer_w, weakCallback);
VROPlatformDispatchAsyncRenderer([arRenderer_w, callbackRef, rayVec] {
performARHitTest(rayVec, arRenderer_w, callbackRef);
});
}

Expand All @@ -270,10 +270,10 @@ VRO_METHOD(void, nativePerformARHitTestWithPoint) (VRO_ARGS
jobject callback) {
std::shared_ptr<VROSceneRenderer> renderer = Renderer::native(native_renderer);
std::weak_ptr<VROSceneRendererARCore> arRenderer_w = std::dynamic_pointer_cast<VROSceneRendererARCore>(renderer);
jweak weakCallback = env->NewWeakGlobalRef(callback);
jobject callbackRef = env->NewGlobalRef(callback);

VROPlatformDispatchAsyncRenderer([env, arRenderer_w, weakCallback, x, y] {
performARHitTestPoint(env, x, y, arRenderer_w, weakCallback);
VROPlatformDispatchAsyncRenderer([env, arRenderer_w, callbackRef, x, y] {
performARHitTestPoint(env, x, y, arRenderer_w, callbackRef);
});
}

Expand Down
Loading
Loading