-
Notifications
You must be signed in to change notification settings - Fork 209
Description
Video recording returns {success: false, errorCode: 0} on iOS
Environment
| Component | Version |
|---|---|
@reactvision/react-viro |
^2.52.0 |
expo |
~54.0.32 |
react-native |
0.81.5 |
| iOS | 18.2 (tested on physical device) |
| Architecture | New Architecture (Fabric) enabled |
| Xcode | 16.x |
Description
Calling startVideoRecording on ViroARSceneNavigator completes without invoking the error callback, but the subsequent call to stopVideoRecording immediately resolves with {success: false, errorCode: 0}. The recorded file URL is empty and no video is saved.
This makes the video recording feature completely non-functional on iOS. Screenshot capture (takeScreenshot) continues to work correctly through the same view registry path.
Steps to Reproduce
- Render a
ViroARSceneNavigatorwith an AR scene containing placed 3D objects. - Call
startVideoRecordingvia the navigator ref:arSceneNavigator._startVideoRecording( 'test_video', // fileName false, // saveToCameraRoll (errorCode) => console.log('Recording error:', errorCode) );
- Wait a few seconds, then call
stopVideoRecording:arSceneNavigator._stopVideoRecording().then((result) => { console.log(result); // Result: { success: false, url: '', errorCode: 0 } });
Expected Behavior
stopVideoRecording should resolve with {success: true} and a valid file URL pointing to the recorded .mp4 file.
Actual Behavior
startVideoRecordingexecutes without triggering the error callback (appears to succeed).stopVideoRecordingimmediately resolves with{success: false, errorCode: 0}.- No video file is created.
Console Output
LOG Starting video recording...
LOG Video recording started
LOG Stopping video recording...
LOG Video recording stopped: {"errorCode": 0, "success": false, "url": ""}
Root Cause Analysis
Error Code Mapping
From VROViewRecorder.h, errorCode: 0 maps to kVROViewErrorUnknown:
static NSInteger const kVROViewErrorNone = -1;
static NSInteger const kVROViewErrorUnknown = 0; // ← returned error
static NSInteger const kVROViewErrorNoPermissions = 1;
static NSInteger const kVROViewErrorInitialization = 2;
// ...The kVROViewErrorUnknown (0) code is the default/fallback error, indicating the recorder never progressed far enough to produce a more specific error. This points to a failure during internal initialization of the recording pipeline rather than a permissions or file-write issue.
The GLKView Dependency
VROViewRecorder is initialized with a GLKView reference and relies on OpenGL ES frame capture to feed pixel data into an AVAssetWriter:
// VROViewRecorder.h
@interface VROViewRecorder : NSObject
- (id)initWithView:(GLKView *)view
renderer:(std::shared_ptr<VRORenderer>)renderer
driver:(std::shared_ptr<VRODriver>)driver;The recorder's didRenderFrame callback (via VROViewRecorderRTTDelegate) reads rendered frames from OpenGL render targets and appends them to the video writer. If the GLKView is not producing valid GL frames — or if the view reference is nil/stale — the AVAssetWriter is never initialized, and stopVideoRecording returns kVROViewErrorUnknown.
Why This Fails on Modern iOS
-
GLKit is deprecated. Apple deprecated
GLKit(includingGLKView) in iOS 12 and has been progressively reducing its functionality. On iOS 17+ and especially iOS 18,GLKViewrendering may not produce valid framebuffer data depending on the Metal/GL interop path. -
OpenGL ES is deprecated. Apple deprecated OpenGL ES in iOS 12 in favor of Metal. The
VROViewRecordercaptures frames by reading from GL render targets (VRORenderToTextureDelegate). If the GL context is not properly configured or the render-to-texture path doesn't produce valid pixel data, the recorder silently fails. -
New Architecture (Fabric) interaction. Under the New Architecture, the view hierarchy and native component instantiation differ from the legacy bridge.
findNodeHandlereturns a Fabric handle, which may not map cleanly to theGLKViewinstance thatVROViewRecorderexpects. This could result in the recorder being initialized with a nil or wrong view reference. -
Precompiled binary limitation.
VROViewRecorderis shipped insideViroKit.frameworkas a precompiled binary. The initialization and frame capture logic cannot be patched or debugged from the consuming application. The failure occurs entirely within the opaque native layer.
Why takeScreenshot Still Works
takeScreenshot captures a single frame synchronously from the current GL state and writes it as a PNG. This simpler path is more tolerant of GL context issues because it doesn't require the sustained frame capture pipeline (AVAssetWriter + AVAssetWriterInput + per-frame pixel buffer appending) that video recording depends on.
Call Flow
JavaScript (ViroARSceneNavigator)
→ RCT Bridge: startVideoRecording / stopVideoRecording
→ VRTARSceneNavigatorModule.mm
→ [VRTARSceneNavigator startVideoRecording:...]
→ [VROViewRecorder startVideoRecording:...]
→ Attempts to set up AVAssetWriter with GLKView frame dimensions
→ Registers VROViewRecorderRTTDelegate for per-frame callbacks
→ ❌ Frame capture pipeline never produces valid frames
→ AVAssetWriter never receives input
→ stopVideoRecording returns kVROViewErrorUnknown (0)
Affected Code Path
| Layer | File | Role |
|---|---|---|
| JS Component | ViroARSceneNavigator.tsx |
Exposes _startVideoRecording / _stopVideoRecording to React |
| Native Module | VRTARSceneNavigatorModule.mm |
Bridges JS calls to the native navigator view |
| Native View | VRTARSceneNavigator |
Holds the VROViewRecorder instance |
| Core Recorder | VROViewRecorder (precompiled in ViroKit.framework) |
Implements GL-based frame capture and AVAssetWriter pipeline |
Suggested Fix
The VROViewRecorder needs to be updated to use a Metal-based frame capture pipeline instead of the deprecated GLKit/OpenGL ES path. Possible approaches:
-
Replace
GLKViewframe reading withMTLTexturereadback. Since ViroKit already uses Metal for rendering on modern iOS, the recorder should capture frames directly from the Metal render pipeline rather than through a GL compatibility layer. -
Use
RPScreenRecorder(ReplayKit). Apple's ReplayKit provides a high-level screen recording API that captures the entire app output without needing access to the GL/Metal framebuffer. This would sidestep the GL deprecation entirely, though it captures the full screen rather than just the AR view. -
Use
ARSessionframe capture.ARFrame.capturedImageprovides camera frames asCVPixelBuffer, which can be combined with the rendered overlay to produce video output without relying on GLKit.
Workaround
We have disabled video recording behind a feature flag (EXPO_PUBLIC_VIDEO_ENABLED=false) until this issue is resolved upstream. Screenshot capture continues to function correctly.