Skip to content

Video recording returns {success: false, errorCode: 0} on iOS #437

@oliedis

Description

@oliedis

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

  1. Render a ViroARSceneNavigator with an AR scene containing placed 3D objects.
  2. Call startVideoRecording via the navigator ref:
    arSceneNavigator._startVideoRecording(
      'test_video',   // fileName
      false,          // saveToCameraRoll
      (errorCode) => console.log('Recording error:', errorCode)
    );
  3. 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

  • startVideoRecording executes without triggering the error callback (appears to succeed).
  • stopVideoRecording immediately 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

  1. GLKit is deprecated. Apple deprecated GLKit (including GLKView) in iOS 12 and has been progressively reducing its functionality. On iOS 17+ and especially iOS 18, GLKView rendering may not produce valid framebuffer data depending on the Metal/GL interop path.

  2. OpenGL ES is deprecated. Apple deprecated OpenGL ES in iOS 12 in favor of Metal. The VROViewRecorder captures 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.

  3. New Architecture (Fabric) interaction. Under the New Architecture, the view hierarchy and native component instantiation differ from the legacy bridge. findNodeHandle returns a Fabric handle, which may not map cleanly to the GLKView instance that VROViewRecorder expects. This could result in the recorder being initialized with a nil or wrong view reference.

  4. Precompiled binary limitation. VROViewRecorder is shipped inside ViroKit.framework as 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:

  1. Replace GLKView frame reading with MTLTexture readback. 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.

  2. 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.

  3. Use ARSession frame capture. ARFrame.capturedImage provides camera frames as CVPixelBuffer, 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions