Skip to content

Feat: Ingest Vision Camera#3

Merged
gmemmy merged 3 commits intomainfrom
feat/vision-camera-ingest
Oct 10, 2025
Merged

Feat: Ingest Vision Camera#3
gmemmy merged 3 commits intomainfrom
feat/vision-camera-ingest

Conversation

@gmemmy
Copy link
Owner

@gmemmy gmemmy commented Sep 25, 2025

Summary by CodeRabbit

  • New Features

    • Live camera integration with start/stop, flip, torch, FPS selection, and per-track backpressure modes.
    • Per-track stats (produced/delivered FPS and dropped frames) and new per-track/frame delivery API.
    • In-app test framework to run camera/RTC integration tests.
  • Improvements

    • UI updates: flexible view component (style/children) and safe-area layout handling.
    • Capability checks with clearer error reporting.
  • Bug Fixes

    • Ensured video renderer cleanup on view teardown to prevent leaks.
  • Chores

    • Added ignore rule for editor .cursor/ folder.

@gmemmy gmemmy self-assigned this Sep 25, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 25, 2025

Walkthrough

Adds Vision Camera external-source support with per-track backpressure, delivery APIs, and per-track stats. Extends iOS native module/spec and JS/TS bridge, reworks VisionRTCView, adds example camera preview, test framework, and project/config hygiene updates.

Changes

Cohort / File(s) Summary of Changes
Repo hygiene
\.gitignore, lefthook.yml
Added .cursor/ to .gitignore; switched pre-commit hooks from npx to yarn invocations.
Example app & wiring
example/index.js, example/src/App.tsx, example/package.json, example/ios/VisionRtcExample/Info.plist, example/ios/VisionRtcExample.xcodeproj/..., example/src/test-framework.tsx, example/src/vision-camera-frame-processor.ts, example/src/*
Wrapped app in SafeAreaProvider; added react-native-vision-camera dependency; added camera/mic usage strings to Info.plist; modified Xcode project config entries; added camera preview, controls (start/stop/flip/torch/fps/backpressure), test framework, and frame-processor module to deliver frames to native.
iOS native bridge & runtime
ios/VisionRTC+Spec.mm, ios/VisionRTCModule.swift, ios/VisionRTCViewManager.m
Added TurboModule spec methods (updateSource, disposeSource, getStatsForTrack, deliverFrame); implemented source binding, per-track backpressure modes/policies, buffering/delivery/drop accounting, deliverExternalFrame logic, per-track stats, and cleanup; added view dealloc cleanup removing renderer.
TypeScript API & types
src/NativeVisionRtc.ts, src/index.ts, src/types.ts
Switched Spec declaration shape; added updateSource/disposeSource/deliverFrame methods; added backpressure option and Backpressure type; renamed replaceTrackreplaceSenderTrack, setTrackConstraintsupdateTrack; changed getStats to accept optional trackId and added getStatsForTrack; exported Capabilities, VisionRtcError, TrackStats; added capability/assert APIs.
RN view component
src/vision-rtc-view.tsx
Reworked VisionRTCView to accept Props (trackId?, style, children), render a native component via requireNativeComponent, overlay children and apply styles; replaced old simple Text-based view.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI as Example UI
  participant VC as VisionCamera (RN)
  participant JS as VisionRTC JS API
  participant TM as TurboModule (ObjC++)
  participant SW as VisionRTC (Swift)
  participant W as WebRTC Track

  rect rgb(240,248,255)
  note over UI,SW: Initialize & bind external source
  UI->>VC: ensurePermissions + mount Camera
  UI->>JS: createVisionCameraSource(cameraRef)
  JS->>TM: createSource(...)
  TM->>SW: bind source -> sourceId
  TM-->>JS: sourceId
  UI->>JS: createWebRTCTrack({sourceId, backpressure, fps})
  JS->>TM: createTrack(opts)
  TM->>SW: createTrack(opts + bind source)
  SW-->>TM: trackId
  TM-->>JS: trackId
  JS-->>UI: render VisionRTCView(trackId)
  end

  rect rgb(245,255,240)
  note over VC,SW: External frame delivery & backpressure
  VC-->>JS: frame -> getNativeBuffer + ts
  JS->>TM: deliverFrame(sourceId, pixelBuffer, ts)
  TM->>SW: deliverFrame -> deliverExternalFrame
  alt backpressure = latest-wins
    SW->>SW: replace buffered frame for track
  else backpressure = throttle
    SW->>SW: gate frames by time
  else drop-late
    SW->>SW: drop frame if busy
  end
  SW->>W: enqueue/frame emitted to renderer
  SW->>SW: update produced/delivered/dropped counters
  end

  rect rgb(255,248,240)
  note over UI,SW: Controls & stats
  UI->>JS: updateSource(sourceId,{position,torch,maxFps})
  JS->>TM: updateSource(...)
  TM->>SW: apply changes, reconfigure bindings
  UI->>JS: updateTrack(trackId,{fps,backpressure})
  JS->>TM: updateTrack(...)
  TM->>SW: set constraints
  UI->>JS: getStats(trackId?)
  JS->>TM: getStatsForTrack / getStats
  TM->>SW: return per-track/global stats
  TM-->>JS: stats
  JS-->>UI: display stats
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

I nudge the lens, flip front to back,
Frames hop through buffers, never slack.
Latest wins or throttle slow,
Drops counted, torch aglow.
A rabbit's cheer for streaming flow! 🥕📷🐇

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title “Feat: Ingest Vision Camera” clearly describes the main feature added—integrating Vision Camera ingestion—without unnecessary detail or noise, matching the core scope of the changeset.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/vision-camera-ingest

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ios/VisionRTCModule.swift (1)

236-252: Dispose track must clean per-source bindings and per-track stats

Leads to stale mappings, retained pixel buffers, and skewed aggregates.

   @objc(disposeTrack:resolver:rejecter:)
   func disposeTrack(trackId: NSString, resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
     stateQueue.async(flags: .barrier) {
       self.activeTrackIds.remove(trackId as String)
       self.trackStates.removeValue(forKey: trackId as String)
       self.tracks.removeValue(forKey: trackId as String)
       self.sources.removeValue(forKey: trackId as String)
       self.capturers.removeValue(forKey: trackId as String)
       self.lastSent.removeValue(forKey: trackId as String)
+      // Unbind from source
+      if let sid = self.trackToSourceId.removeValue(forKey: trackId as String) {
+        var set = self.sourceToTrackIds[sid] ?? Set<String>()
+        set.remove(trackId as String)
+        if set.isEmpty {
+          self.sourceToTrackIds.removeValue(forKey: sid)
+        } else {
+          self.sourceToTrackIds[sid] = set
+        }
+      }
+      // Clear per-track buffers and stats
+      self.latestBufferByTrack.removeValue(forKey: trackId as String)
+      self.producedThisSecond.removeValue(forKey: trackId as String)
+      self.deliveredThisSecond.removeValue(forKey: trackId as String)
+      self.producedFpsByTrack.removeValue(forKey: trackId as String)
+      self.deliveredFpsByTrack.removeValue(forKey: trackId as String)
+      self.droppedFramesByTrack.removeValue(forKey: trackId as String)
+      self.lastSecondWallClock.removeValue(forKey: trackId as String)
+      self.pausedForReconfig.remove(trackId as String)
     }
🧹 Nitpick comments (7)
example/ios/VisionRtcExample/Info.plist (1)

52-55: iOS privacy strings added correctly.

Camera/mic usage descriptions are present. If you don’t request mic permission in the example, you can defer NSMicrophoneUsageDescription to avoid review noise.

example/index.js (1)

6-12: Safe area provider at root: LGTM.

Optional: pass initialWindowMetrics if you want to avoid first-render inset jumps on cold start.

src/vision-rtc-view.tsx (1)

33-41: Avoid hard-coded absolute positioning in container.

Hard-coding absolute fill on the container can surprise consumers and complicate layout. Let the parent control positioning.

Apply:

 const styles = StyleSheet.create({
   container: {
-    flex: 1,
-    position: 'absolute',
-    top: 0,
-    left: 0,
-    right: 0,
-    bottom: 0,
+    flex: 1,
   },
 });
example/src/App.tsx (1)

173-201: Optional: disable controls based on state.

Improve UX and avoid redundant calls by disabling buttons when not applicable (e.g., Start while creating/active, Stop when idle, Flip/Torch without source, FPS/Backpressure without track).

Example:

-          <Button title="Start" onPress={onStart} />
+          <Button title="Start" onPress={onStart} disabled={creating || !!trackId || !device} />
...
-          <Button title="Stop" onPress={onStop} />
+          <Button title="Stop" onPress={onStop} disabled={!trackId && !sourceId} />
...
-          <Button title="Flip" onPress={onFlip} />
+          <Button title="Flip" onPress={onFlip} disabled={!sourceId} />
...
-          <Button title="15 fps" onPress={() => onFps(15)} />
+          <Button title="15 fps" onPress={() => onFps(15)} disabled={!trackId} />
src/index.ts (2)

116-126: Capabilities may misreport hardware encoder availability

Hardcoding {h264: true, vp8: true} can mislead on devices without those encoders. Consider detecting via native factories or defaulting to false until probed.


128-144: Throwing plain object, not Error

If callers expect instanceof Error, consider throwing an Error subclass carrying the code. Otherwise keep as-is.

-    const err: VisionRtcError = {
-      code: 'ERR_EXPO_GO',
-      message: 'Expo Go is not supported. Use Expo Dev Client.',
-    };
-    throw err;
+    const e = new Error('Expo Go is not supported. Use Expo Dev Client.') as Error & VisionRtcError;
+    (e as any).code = 'ERR_EXPO_GO';
+    throw e;
ios/VisionRTCModule.swift (1)

564-569: SwiftLint: unused closure parameter

fpsNow is unused. Replace with _ to silence the warning.

-    }, onFps: { [weak self] fpsNow, dropped in
+    }, onFps: { [weak self] _, dropped in
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e1f4cba and 1def3a8.

⛔ Files ignored due to path filters (2)
  • example/ios/Podfile.lock is excluded by !**/*.lock
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (12)
  • .gitignore (1 hunks)
  • example/index.js (1 hunks)
  • example/ios/VisionRtcExample/Info.plist (1 hunks)
  • example/package.json (1 hunks)
  • example/src/App.tsx (4 hunks)
  • ios/VisionRTC+Spec.mm (5 hunks)
  • ios/VisionRTCModule.swift (10 hunks)
  • ios/VisionRTCViewManager.m (1 hunks)
  • src/NativeVisionRtc.ts (2 hunks)
  • src/index.ts (4 hunks)
  • src/types.ts (2 hunks)
  • src/vision-rtc-view.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (9)
src/vision-rtc-view.tsx (2)
src/index.ts (1)
  • VisionRTCView (25-25)
android/src/main/java/com/visionrtc/VisionRtcViewManager.kt (2)
  • name (12-42)
  • name (28-36)
src/NativeVisionRtc.ts (1)
android/src/main/java/com/visionrtc/VisionRtcModule.kt (1)
  • source (48-53)
src/types.ts (2)
src/index.ts (3)
  • Capabilities (20-20)
  • VisionRtcError (21-21)
  • TrackStats (22-22)
android/src/main/java/com/visionrtc/VisionRtcModule.kt (1)
  • source (48-53)
example/src/App.tsx (5)
ios/VisionRTCModule.swift (5)
  • createVisionCameraSource (57-68)
  • disposeTrack (236-252)
  • disposeSource (102-109)
  • getStats (254-267)
  • updateSource (70-100)
src/index.ts (7)
  • createVisionCameraSource (27-31)
  • createWebRTCTrack (44-49)
  • disposeTrack (73-75)
  • disposeSource (40-42)
  • getStats (77-95)
  • updateSource (33-38)
  • VisionRTCView (25-25)
android/src/main/java/com/visionrtc/VisionRtcModule.kt (4)
  • createVisionCameraSource (63-68)
  • disposeTrack (198-207)
  • getStats (209-214)
  • name (27-240)
src/vision-rtc-view.tsx (1)
  • VisionRTCView (21-31)
android/src/main/java/com/visionrtc/VisionRtcViewManager.kt (1)
  • name (12-42)
ios/VisionRTC+Spec.mm (1)
ios/VisionRTCTrackRegistry.swift (1)
  • VisionRTCTrackRegistry (4-30)
ios/VisionRTCModule.swift (3)
src/index.ts (2)
  • updateSource (33-38)
  • disposeSource (40-42)
android/src/main/java/com/visionrtc/VisionRtcModule.kt (2)
  • createTrack (70-123)
  • source (48-53)
ios/VisionRTCTrackRegistry.swift (2)
  • track (23-29)
  • register (11-15)
example/index.js (1)
example/src/App.tsx (1)
  • App (16-204)
ios/VisionRTCViewManager.m (3)
ios/VisionRTCTrackRegistry.swift (2)
  • unregister (17-21)
  • VisionRTCTrackRegistry (4-30)
android/src/main/java/com/visionrtc/VisionRtcViewManager.kt (1)
  • name (12-42)
android/src/main/java/com/visionrtc/VisionRtcModule.kt (1)
  • disposeTrack (198-207)
src/index.ts (3)
ios/VisionRTCModule.swift (5)
  • updateSource (70-100)
  • disposeSource (102-109)
  • replaceSenderTrack (162-166)
  • disposeTrack (236-252)
  • getStats (254-267)
android/src/main/java/com/visionrtc/VisionRtcModule.kt (4)
  • source (48-53)
  • replaceSenderTrack (132-134)
  • disposeTrack (198-207)
  • getStats (209-214)
src/types.ts (6)
  • VisionCameraSource (13-13)
  • TrackOptions (3-11)
  • VisionRTCTrack (37-39)
  • TrackStats (62-69)
  • Capabilities (43-49)
  • VisionRtcError (57-60)
🪛 SwiftLint (0.57.0)
ios/VisionRTCModule.swift

[Warning] 564-564: Unused parameter in a closure should be replaced with _

(unused_closure_parameter)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-ios
  • GitHub Check: build-android
🔇 Additional comments (15)
.gitignore (1)

9-11: Ignore Cursor workspace artifacts

Adding .cursor/ keeps the repo clean of Cursor IDE metadata. ✅

ios/VisionRTCViewManager.m (1)

11-16: Good: prevent renderer leaks on unmount.

Detaching the renderer in dealloc avoids dangling sinks. Solid cleanup.

src/types.ts (4)

6-11: Backpressure added to TrackOptions: LGTM.

Type union aligns with usages across the PR.


41-49: New public types (Backpressure/Capabilities): LGTM.

Clear, minimal surface.


51-61: VisionRtcError types: LGTM.

Consistent error surface for consumers.


62-69: TrackStats shape: LGTM.

Matches getStats mapping behavior.

src/NativeVisionRtc.ts (1)

48-53: Verify Android omission of getStatsForTrack is intentional. iOS implements it in ios/VisionRTCModule.swift (line 517), but no Kotlin/Java implementation exists—index.ts will fall back.

src/index.ts (3)

33-38: Source management API looks good

Runtime source updates/disposal are correctly wired to native module.


77-95: Confirm Android fallback used
getStatsForTrack is only implemented in the iOS native module; Android lacks this method, so the fallback path will correctly engage.


66-71: No direct calls to setTrackConstraints outside updateTrack

ios/VisionRTC+Spec.mm (2)

205-216: Per-track stats bridge LGTM

Bridged method forwards to Swift with proper Swift-availability guard.


218-244: Source lifecycle bridge LGTM

updateSource/disposeSource are correctly bridged with Swift-availability checks.

ios/VisionRTCModule.swift (3)

70-100: Source update/reset logic LGTM

Per-track pause, buffer reset, and timed unpause after reconfig are sound.


517-528: Per-track stats API LGTM

Consistent with TS TrackStats and JS surface.


418-516: External frame backpressure path looks correct

Cadencing and per-track counters are guarded and rolled per second; timestamp selection for latest-wins is sensible.

Comment on lines +18 to +19
const cameraRef = React.useRef<Camera>(null);
const device = useCameraDevice('back');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Type error: useRef should allow null.

This won’t type-check in TS.

Apply:

-  const cameraRef = React.useRef<Camera>(null);
+  const cameraRef = React.useRef<Camera | null>(null);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const cameraRef = React.useRef<Camera>(null);
const device = useCameraDevice('back');
const cameraRef = React.useRef<Camera | null>(null);
const device = useCameraDevice('back');
🤖 Prompt for AI Agents
In example/src/App.tsx around lines 18-19, the useRef generic currently excludes
null which causes a TypeScript type error; update the ref type to allow null
(e.g., change the generic to Camera | null) so the initial null value
type-checks, and ensure any usages are null-checked or non-null asserted where
appropriate.

Comment on lines +47 to 62
await ensurePermissions();
const node = findNodeHandle(cameraRef.current);
if (!node) throw new Error('Camera view not ready');
const {__nativeSourceId} = await createVisionCameraSource(node);
setSourceId(__nativeSourceId);
const created = await createWebRTCTrack(
{__nativeSourceId},
{
fps: 30,
resolution: {width: 1280, height: 720},
backpressure,
}
);
newId = id;
setTrackId(id);
newId = created.trackId;
setTrackId(created.trackId);
} catch (err) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Dispose source on failure to avoid leaks.

If track creation fails after creating the Vision source, the source is never disposed.

Apply:

   setCreating(true);
   let newId: string | null = null;
+  let createdSourceId: string | null = null;
   try {
     await ensurePermissions();
     const node = findNodeHandle(cameraRef.current);
     if (!node) throw new Error('Camera view not ready');
-    const {__nativeSourceId} = await createVisionCameraSource(node);
-    setSourceId(__nativeSourceId);
+    const {__nativeSourceId} = await createVisionCameraSource(node);
+    createdSourceId = __nativeSourceId;
+    setSourceId(__nativeSourceId);
     const created = await createWebRTCTrack(
       {__nativeSourceId},
       {
         fps: 30,
         resolution: {width: 1280, height: 720},
         backpressure,
       }
     );
     newId = created.trackId;
     setTrackId(created.trackId);
   } catch (err) {
     if (newId) {
       try {
         await disposeTrack(newId);
       } catch {}
     }
+    if (createdSourceId) {
+      try {
+        await disposeSource(createdSourceId);
+      } catch {}
+    }
     console.error('Failed to start WebRTC track', err);
   } finally {
     setCreating(false);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await ensurePermissions();
const node = findNodeHandle(cameraRef.current);
if (!node) throw new Error('Camera view not ready');
const {__nativeSourceId} = await createVisionCameraSource(node);
setSourceId(__nativeSourceId);
const created = await createWebRTCTrack(
{__nativeSourceId},
{
fps: 30,
resolution: {width: 1280, height: 720},
backpressure,
}
);
newId = id;
setTrackId(id);
newId = created.trackId;
setTrackId(created.trackId);
} catch (err) {
setCreating(true);
let newId: string | null = null;
let createdSourceId: string | null = null;
try {
await ensurePermissions();
const node = findNodeHandle(cameraRef.current);
if (!node) throw new Error('Camera view not ready');
const {__nativeSourceId} = await createVisionCameraSource(node);
createdSourceId = __nativeSourceId;
setSourceId(__nativeSourceId);
const created = await createWebRTCTrack(
{__nativeSourceId},
{
fps: 30,
resolution: {width: 1280, height: 720},
backpressure,
}
);
newId = created.trackId;
setTrackId(created.trackId);
} catch (err) {
if (newId) {
try {
await disposeTrack(newId);
} catch {}
}
if (createdSourceId) {
try {
await disposeSource(createdSourceId);
} catch {}
}
console.error('Failed to start WebRTC track', err);
} finally {
setCreating(false);
}
🤖 Prompt for AI Agents
In example/src/App.tsx around lines 47 to 62, the Vision camera source created
via createVisionCameraSource is not disposed if subsequent createWebRTCTrack
fails, leaking native resources; update the try/catch (or add a finally) to
store the created __nativeSourceId in an outer variable and, on error or in
finally when track creation did not complete, call and await the appropriate
source-disposal API (e.g., disposeVisionCameraSource or equivalent) only if
__nativeSourceId exists, then clear the variable; ensure disposal errors are
caught/logged but do not mask the original error.

Comment on lines 207 to 213
container: {
flex: 1,
backgroundColor: '#ffff',
justifyContent: 'center',
alignItems: 'center',
},
controls: {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Color value typo.

'#ffff' is unusual; use '#fff' or '#ffffff'.

Apply:

-    backgroundColor: '#ffff',
+    backgroundColor: '#fff',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
container: {
flex: 1,
backgroundColor: '#ffff',
justifyContent: 'center',
alignItems: 'center',
},
controls: {
container: {
flex: 1,
backgroundColor: '#fff',
justifyContent: 'center',
alignItems: 'center',
},
controls: {
🤖 Prompt for AI Agents
In example/src/App.tsx around lines 207 to 213, the container style uses an
invalid/typo color value '#ffff'; replace it with a valid 3- or 6-digit hex like
'#fff' or '#ffffff' (e.g., set backgroundColor to '#ffffff') so the style uses a
correct hex color.

Comment on lines +105 to 109
NSString *bp = opts.backpressure();
if (bp != nil) {
optsDict[@"backpressure"] = bp;
}
[self.swift createTrack:source opts:optsDict resolver:resolve rejecter:reject];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Compile-time bug: incorrect reading of optional backpressure

opts.backpressure() returns an optional; assigning to NSString * is invalid. Use the same pattern as fps()/resolution().

-  NSString *bp = opts.backpressure();
-  if (bp != nil) {
-    optsDict[@"backpressure"] = bp;
-  }
+  if (auto bp = opts.backpressure()) {
+    optsDict[@"backpressure"] = *bp;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
NSString *bp = opts.backpressure();
if (bp != nil) {
optsDict[@"backpressure"] = bp;
}
[self.swift createTrack:source opts:optsDict resolver:resolve rejecter:reject];
if (auto bp = opts.backpressure()) {
optsDict[@"backpressure"] = *bp;
}
[self.swift createTrack:source opts:optsDict resolver:resolve rejecter:reject];
🤖 Prompt for AI Agents
In ios/VisionRTC+Spec.mm around lines 105–109, opts.backpressure() returns an
optional and cannot be assigned directly to NSString *; follow the same pattern
as fps()/resolution() by capturing the optional (e.g., auto bp =
opts.backpressure()), check whether it has a value, and if so
extract/dereference the contained NSString and assign that to
optsDict[@"backpressure"] before calling createTrack; update the conditional
accordingly.

Comment on lines +173 to +176
NSString *bp = opts.backpressure();
if (bp != nil) {
optsDict[@"backpressure"] = bp;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Same optional handling bug in setTrackConstraints

Mirror the fix here too.

-  NSString *bp = opts.backpressure();
-  if (bp != nil) {
-    optsDict[@"backpressure"] = bp;
-  }
+  if (auto bp = opts.backpressure()) {
+    optsDict[@"backpressure"] = *bp;
+  }
🤖 Prompt for AI Agents
In ios/VisionRTC+Spec.mm around lines 173-176, the optional backpressure string
is being assigned without guarding against null/CFNull; mirror the previous fix
by retrieving the value into NSString *bp = opts.backpressure(); then only set
optsDict[@"backpressure"] when bp is non-nil and not kCFNull (e.g. if (bp != nil
&& bp != (id)kCFNull) { optsDict[@"backpressure"] = bp; }), ensuring you avoid
inserting null placeholders into the dictionary.

Comment on lines +22 to 27
readonly updateSource: (
sourceId: string,
opts: {position?: 'front' | 'back'; torch?: boolean; maxFps?: number}
) => Promise<void>;
readonly disposeSource: (sourceId: string) => Promise<void>;
readonly createTrack: (
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

New source APIs declared; confirm native parity.

Ensure both iOS and Android implement updateSource/disposeSource; Android snippets didn’t show them.

Run:


🏁 Script executed:

#!/bin/bash
rg -n -C2 --type=kt '\boverride fun (updateSource|disposeSource)\b'
rg -n -C2 --type=swift '@objc\((updateSource|disposeSource):'

Length of output: 721


🏁 Script executed:

#!/bin/bash
# Search Android implementation for updateSource and disposeSource in Kotlin or Java
rg -nC2 '\bupdateSource\b' -g '*.kt' -g '*.java'
rg -nC2 '\bdisposeSource\b' -g '*.kt' -g '*.java'

Length of output: 0


Implement updateSource and disposeSource in Android native module
iOS defines updateSource(_:opts:resolver:rejecter:) and disposeSource(_:resolver:rejecter:), but no Android counterparts were found. Add these @ReactMethod implementations in your Android VisionRTCModule to ensure parity.

🤖 Prompt for AI Agents
In src/NativeVisionRtc.ts around lines 22 to 27, the TypeScript interface
declares updateSource and disposeSource but the Android native module lacks
corresponding implementations; add two @ReactMethod functions in the Android
VisionRTCModule: updateSource(String sourceId, ReadableMap opts, Promise
promise) and disposeSource(String sourceId, Promise promise). Inside
updateSource, validate sourceId, extract opts.position/torch/maxFps from the
ReadableMap, apply changes to the underlying source object (update camera
position, toggle torch, adjust fps) with proper thread/context handling, and
resolve the promise on success or reject with a clear error on failure. In
disposeSource, validate sourceId, perform cleanup/removal of the source from the
module’s tracking structures, release resources on the correct thread, and
resolve or reject the promise accordingly so behavior matches the existing iOS
methods.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
example/src/App.tsx (2)

20-205: Hook the camera device to the facing state
Flip toggles facing, but useCameraDevice('back') never re-runs, so the preview (and Vision camera source) stays on the back camera. Derive the device from facing instead.

-  const cameraRef = React.useRef<Camera>(null);
-  const device = useCameraDevice('back');
+  const cameraRef = React.useRef<Camera | null>(null);
+  const [facing, setFacing] = React.useState<'front' | 'back'>('back');
+  const device = useCameraDevice(facing);
@@
-  const [torch, setTorch] = React.useState(false);
-  const [facing, setFacing] = React.useState<'front' | 'back'>('back');
+  const [torch, setTorch] = React.useState(false);

(Move the facing state before useCameraDevice so the hook has access to the current value.)


90-99: Dispose the source on unmount
Unmount cleanup only tears down the track; any active Vision camera source survives, leaking native state. Mirror the onStop logic and dispose the source too.

-  React.useEffect(() => {
-    return () => {
-      if (trackId) {
-        disposeTrack(trackId).catch(() => {});
-      }
-    };
-  }, [trackId]);
+  React.useEffect(() => {
+    return () => {
+      if (trackId) {
+        disposeTrack(trackId).catch(() => {});
+      }
+      if (sourceId) {
+        disposeSource(sourceId).catch(() => {});
+      }
+    };
+  }, [trackId, sourceId]);
ios/VisionRTCModule.swift (1)

246-266: Remove disposed tracks from source bindings

disposeTrack clears the track but leaves trackToSourceId and sourceToTrackIds entries behind. Over time this builds stale bindings that keep delivering attempts and retains memory. Drop the mapping when the track goes away.

     stateQueue.async(flags: .barrier) {
       self.activeTrackIds.remove(trackId as String)
       self.trackStates.removeValue(forKey: trackId as String)
       self.tracks.removeValue(forKey: trackId as String)
       self.sources.removeValue(forKey: trackId as String)
       self.capturers.removeValue(forKey: trackId as String)
       self.lastSent.removeValue(forKey: trackId as String)
+      if let sourceId = self.trackToSourceId.removeValue(forKey: trackId as String) {
+        var set = self.sourceToTrackIds[sourceId] ?? Set<String>()
+        set.remove(trackId as String)
+        if set.isEmpty {
+          self.sourceToTrackIds.removeValue(forKey: sourceId)
+        } else {
+          self.sourceToTrackIds[sourceId] = set
+        }
+      }
       self.latestBufferByTrack.removeValue(forKey: trackId as String)
       self.producedThisSecond.removeValue(forKey: trackId as String)
       self.deliveredThisSecond.removeValue(forKey: trackId as String)
       self.lastSecondWallClock.removeValue(forKey: trackId as String)
       self.deliveredFpsByTrack.removeValue(forKey: trackId as String)
       self.producedFpsByTrack.removeValue(forKey: trackId as String)
       self.droppedFramesByTrack.removeValue(forKey: trackId as String)
     }
♻️ Duplicate comments (5)
src/NativeVisionRtc.ts (1)

22-27: Android implementation missing for new source APIs

Lines 22‑27 extend the native Spec with updateSource/disposeSource, but VisionRtcModule.kt still lacks the corresponding overrides (confirmed by the current Android source). Since the Kotlin class subclasses NativeVisionRtcSpec, the project now fails to compile on Android and JS calls would reject even if you stubbed them. Add @ReactMethod implementations for updateSource and disposeSource (plus wire their behavior) before landing.

ios/VisionRTC+Spec.mm (2)

110-113: Fix optional backpressure extraction in createTrack

opts.backpressure() returns an optional; assigning it straight to NSString * causes a compile error. Mirror the fps/resolution pattern:

-  NSString *bp = opts.backpressure();
-  if (bp != nil) {
-    optsDict[@"backpressure"] = bp;
-  }
+  if (auto bp = opts.backpressure()) {
+    optsDict[@"backpressure"] = *bp;
+  }

Without this, the iOS target won’t build.


178-181: Apply the same optional fix in setTrackConstraints

Line 178 repeats the optional assignment problem, so setTrackConstraints also fails to compile. Use the same if (auto bp = opts.backpressure()) { … } pattern here.

example/src/App.tsx (2)

31-145: Dispose the created source when track creation fails
If createWebRTCTrack throws after createVisionCameraSource resolves, the native source leaks and sourceId state still points to it. Capture the source id, dispose it inside the catch, and clear the React state (already requested earlier).

-  const onStart = async () => {
+  const onStart = async () => {
     if (creating || trackId) return;
     setCreating(true);
     let newId: string | null = null;
+    let createdSourceId: string | null = null;
     try {
       await ensurePermissions();
       const node = findNodeHandle(cameraRef.current);
       if (!node) throw new Error('Camera view not ready');
-      const {__nativeSourceId} = await createVisionCameraSource(node);
-      setSourceId(__nativeSourceId);
+      const {__nativeSourceId} = await createVisionCameraSource(node);
+      createdSourceId = __nativeSourceId;
+      setSourceId(__nativeSourceId);
       const created = await createWebRTCTrack(
         {__nativeSourceId},
         {
           fps: 30,
           resolution: {width: 1280, height: 720},
           backpressure,
         }
       );
       newId = created.trackId;
       setTrackId(created.trackId);
     } catch (err) {
       if (newId) {
         try {
           await disposeTrack(newId);
         } catch {}
       }
+      if (createdSourceId) {
+        try {
+          await disposeSource(createdSourceId);
+        } catch {}
+        setSourceId(null);
+      }
       console.error('Failed to start WebRTC track', err);
     } finally {
       setCreating(false);
     }
   };

19-21: Fix the cameraRef typing
The ref is initialised with null; forcing it to Camera still violates strict null checks and keeps the TypeScript error alive (previously flagged).

-  const cameraRef = React.useRef<Camera>(null);
+  const cameraRef = React.useRef<Camera | null>(null);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1def3a8 and 7fd5004.

📒 Files selected for processing (9)
  • example/ios/VisionRtcExample.xcodeproj/project.pbxproj (2 hunks)
  • example/src/App.tsx (4 hunks)
  • example/src/test-framework.tsx (1 hunks)
  • example/src/vision-camera-frame-processor.ts (1 hunks)
  • ios/VisionRTC+Spec.mm (5 hunks)
  • ios/VisionRTCModule.swift (12 hunks)
  • lefthook.yml (1 hunks)
  • src/NativeVisionRtc.ts (2 hunks)
  • src/index.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
example/src/test-framework.tsx (3)
example/src/vision-camera-frame-processor.ts (4)
  • processFrame (17-47)
  • setSourceId (7-10)
  • getFrameCount (49-51)
  • clearSourceId (12-15)
ios/VisionRTCModule.swift (4)
  • createVisionCameraSource (57-68)
  • getStats (270-283)
  • disposeTrack (245-268)
  • disposeSource (102-116)
src/index.ts (5)
  • createVisionCameraSource (27-31)
  • createWebRTCTrack (44-49)
  • getStats (88-106)
  • disposeTrack (73-75)
  • disposeSource (40-42)
example/src/vision-camera-frame-processor.ts (2)
ios/VisionRTCModule.swift (1)
  • deliverFrame (555-585)
src/index.ts (1)
  • deliverFrame (77-86)
ios/VisionRTCModule.swift (3)
src/index.ts (3)
  • updateSource (33-38)
  • disposeSource (40-42)
  • deliverFrame (77-86)
android/src/main/java/com/visionrtc/VisionRtcModule.kt (2)
  • createTrack (70-123)
  • source (48-53)
ios/VisionRTCTrackRegistry.swift (2)
  • track (23-29)
  • register (11-15)
src/index.ts (4)
ios/VisionRTCModule.swift (6)
  • updateSource (70-100)
  • disposeSource (102-116)
  • replaceSenderTrack (169-173)
  • disposeTrack (245-268)
  • deliverFrame (555-585)
  • getStats (270-283)
android/src/main/java/com/visionrtc/VisionRtcModule.kt (4)
  • source (48-53)
  • replaceSenderTrack (132-134)
  • disposeTrack (198-207)
  • getStats (209-214)
src/types.ts (7)
  • VisionCameraSource (13-13)
  • NativePixelSource (32-35)
  • TrackOptions (3-11)
  • VisionRTCTrack (37-39)
  • TrackStats (62-69)
  • Capabilities (43-49)
  • VisionRtcError (57-60)
src/vision-rtc-view.tsx (1)
  • VisionRTCView (3-9)
ios/VisionRTC+Spec.mm (1)
ios/VisionRTCTrackRegistry.swift (1)
  • VisionRTCTrackRegistry (4-30)
src/NativeVisionRtc.ts (1)
android/src/main/java/com/visionrtc/VisionRtcModule.kt (2)
  • source (48-53)
  • name (27-240)
🪛 GitHub Check: lint
example/src/test-framework.tsx

[failure] 199-199:
Argument of type '(value: unknown) => void' is not assignable to parameter of type '() => void'.


[failure] 1-1:
'React' is declared but its value is never read.

🪛 SwiftLint (0.57.0)
ios/VisionRTCModule.swift

[Warning] 621-621: Unused parameter in a closure should be replaced with _

(unused_closure_parameter)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-ios
  • GitHub Check: build-android

Comment on lines +1 to +15
import React, {useState, useRef, useCallback, useEffect} from 'react';
import {View, Text, Button, StyleSheet, ScrollView, Alert} from 'react-native';
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
import {
Camera,
useCameraDevice,
useFrameProcessor,
} from 'react-native-vision-camera';
import {
createVisionCameraSource,
createWebRTCTrack,
disposeTrack,
disposeSource,
getStats,
} from 'react-native-vision-rtc';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove unused default React import

Lint is failing with 'React' is declared but its value is never read. Drop the default import so the pipeline passes.

-import React, {useState, useRef, useCallback, useEffect} from 'react';
+import {useState, useRef, useCallback, useEffect} from 'react';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import React, {useState, useRef, useCallback, useEffect} from 'react';
import {View, Text, Button, StyleSheet, ScrollView, Alert} from 'react-native';
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
import {
Camera,
useCameraDevice,
useFrameProcessor,
} from 'react-native-vision-camera';
import {
createVisionCameraSource,
createWebRTCTrack,
disposeTrack,
disposeSource,
getStats,
} from 'react-native-vision-rtc';
// example/src/test-framework.tsx
import {useState, useRef, useCallback, useEffect} from 'react';
import {View, Text, Button, StyleSheet, ScrollView, Alert} from 'react-native';
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
import {
Camera,
useCameraDevice,
useFrameProcessor,
} from 'react-native-vision-camera';
import {
createVisionCameraSource,
createWebRTCTrack,
disposeTrack,
disposeSource,
getStats,
} from 'react-native-vision-rtc';
🧰 Tools
🪛 GitHub Check: lint

[failure] 1-1:
'React' is declared but its value is never read.

🤖 Prompt for AI Agents
In example/src/test-framework.tsx around lines 1 to 15, the default React import
is unused causing a lint error; remove the default import token "React" from the
import statement and keep the named imports (e.g. import {useState, useRef,
useCallback, useEffect} from 'react';) so the file uses only the consumed hooks
and the linter error is resolved.

Comment on lines +43 to +44
const cameraRef = useRef<Camera>(null);
const device = useCameraDevice('back');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Allow cameraRef to store null

useRef<Camera>(null) violates the type contract under strictNullChecks and breaks the TypeScript build. Let the ref accept null.

-  const cameraRef = useRef<Camera>(null);
+  const cameraRef = useRef<Camera | null>(null);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const cameraRef = useRef<Camera>(null);
const device = useCameraDevice('back');
const cameraRef = useRef<Camera | null>(null);
const device = useCameraDevice('back');
🤖 Prompt for AI Agents
In example/src/test-framework.tsx around lines 43 to 44, the ref is declared as
useRef<Camera>(null) which violates strictNullChecks; change the ref type to
allow null (e.g., useRef<Camera | null>(null) or the equivalent RefObject type)
so the ref can store null without breaking the TypeScript build.

Comment on lines +198 to +201
while (attempts < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, 100));
const currentFrameCount = getFrameCount();

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix setTimeout callback signature

Passing resolve directly gives it the type (value: unknown) => void, which doesn’t satisfy () => void and triggers the lint error. Wrap the call so the handler has the right shape.

-      await new Promise((resolve) => setTimeout(resolve, 100));
+      await new Promise<void>((resolve) =>
+        setTimeout(() => resolve(), 100)
+      );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while (attempts < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, 100));
const currentFrameCount = getFrameCount();
while (attempts < maxAttempts) {
await new Promise<void>((resolve) =>
setTimeout(() => resolve(), 100)
);
const currentFrameCount = getFrameCount();
🧰 Tools
🪛 GitHub Check: lint

[failure] 199-199:
Argument of type '(value: unknown) => void' is not assignable to parameter of type '() => void'.

🤖 Prompt for AI Agents
In example/src/test-framework.tsx around lines 198 to 201, the setTimeout call
passes resolve directly which gives the callback the wrong signature; wrap
resolve in a zero-argument function so the timer handler is () => void (e.g.,
setTimeout(() => resolve(), 100)) to satisfy the expected callback type and
eliminate the lint error.

Comment on lines +1 to +47
import type {Frame} from 'react-native-vision-camera';
import {deliverFrame} from 'react-native-vision-rtc';

let sourceId: string | null = null;
let frameCount = 0;

export function setSourceId(id: string) {
sourceId = id;
frameCount = 0;
}

export function clearSourceId() {
sourceId = null;
frameCount = 0;
}

export function processFrame(frame: Frame): void {
'worklet';

if (!sourceId) {
console.log('VisionCamera: No source ID set, skipping frame delivery');
return;
}

frameCount++;

// Only deliver every 2nd frame to avoid overwhelming the system during testing
if (frameCount % 2 !== 0) {
return;
}

try {
// Get the native pixel buffer reference
const pixelBufferRef = (frame as Frame).getNativeBuffer();
const timestampNs = frame.timestamp * 1000000; // Convert to nanoseconds

if (pixelBufferRef) {
deliverFrame(sourceId, pixelBufferRef, timestampNs).catch(
(error: unknown) => {
console.warn('VisionCamera: Failed to deliver frame:', error);
}
);
}
} catch (error) {
console.warn('VisionCamera: Error processing frame:', error);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Route deliverFrame through runOnJS

Frame processors run in a worklet; calling a regular JS promise (deliverFrame) directly will throw at runtime. Bounce back to JS via runOnJS and keep the existing error handling there.

-import type {Frame} from 'react-native-vision-camera';
-import {deliverFrame} from 'react-native-vision-rtc';
+import type {Frame} from 'react-native-vision-camera';
+import {runOnJS} from 'react-native-reanimated';
+import {deliverFrame} from 'react-native-vision-rtc';
 
 let sourceId: string | null = null;
 let frameCount = 0;
 
+function deliverFrameFromWorklet(
+  id: string,
+  pixelBuffer: unknown,
+  timestampNs: number
+) {
+  deliverFrame(id, pixelBuffer, timestampNs).catch((error: unknown) => {
+    console.warn('VisionCamera: Failed to deliver frame:', error);
+  });
+}
+
 export function setSourceId(id: string) {
   sourceId = id;
   frameCount = 0;
 }
@@
-    if (pixelBufferRef) {
-      deliverFrame(sourceId, pixelBufferRef, timestampNs).catch(
-        (error: unknown) => {
-          console.warn('VisionCamera: Failed to deliver frame:', error);
-        }
-      );
-    }
+    if (pixelBufferRef) {
+      runOnJS(deliverFrameFromWorklet)(
+        sourceId,
+        pixelBufferRef,
+        timestampNs
+      );
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type {Frame} from 'react-native-vision-camera';
import {deliverFrame} from 'react-native-vision-rtc';
let sourceId: string | null = null;
let frameCount = 0;
export function setSourceId(id: string) {
sourceId = id;
frameCount = 0;
}
export function clearSourceId() {
sourceId = null;
frameCount = 0;
}
export function processFrame(frame: Frame): void {
'worklet';
if (!sourceId) {
console.log('VisionCamera: No source ID set, skipping frame delivery');
return;
}
frameCount++;
// Only deliver every 2nd frame to avoid overwhelming the system during testing
if (frameCount % 2 !== 0) {
return;
}
try {
// Get the native pixel buffer reference
const pixelBufferRef = (frame as Frame).getNativeBuffer();
const timestampNs = frame.timestamp * 1000000; // Convert to nanoseconds
if (pixelBufferRef) {
deliverFrame(sourceId, pixelBufferRef, timestampNs).catch(
(error: unknown) => {
console.warn('VisionCamera: Failed to deliver frame:', error);
}
);
}
} catch (error) {
console.warn('VisionCamera: Error processing frame:', error);
}
}
import type {Frame} from 'react-native-vision-camera';
import {runOnJS} from 'react-native-reanimated';
import {deliverFrame} from 'react-native-vision-rtc';
let sourceId: string | null = null;
let frameCount = 0;
function deliverFrameFromWorklet(
id: string,
pixelBuffer: unknown,
timestampNs: number
) {
deliverFrame(id, pixelBuffer, timestampNs).catch((error: unknown) => {
console.warn('VisionCamera: Failed to deliver frame:', error);
});
}
export function setSourceId(id: string) {
sourceId = id;
frameCount = 0;
}
export function clearSourceId() {
sourceId = null;
frameCount = 0;
}
export function processFrame(frame: Frame): void {
'worklet';
if (!sourceId) {
console.log('VisionCamera: No source ID set, skipping frame delivery');
return;
}
frameCount++;
// Only deliver every 2nd frame to avoid overwhelming the system during testing
if (frameCount % 2 !== 0) {
return;
}
try {
// Get the native pixel buffer reference
const pixelBufferRef = (frame as Frame).getNativeBuffer();
const timestampNs = frame.timestamp * 1000000; // Convert to nanoseconds
if (pixelBufferRef) {
runOnJS(deliverFrameFromWorklet)(
sourceId,
pixelBufferRef,
timestampNs
);
}
} catch (error) {
console.warn('VisionCamera: Error processing frame:', error);
}
}

Comment on lines +111 to +123
const Constants = require('expo-constants').default;
return Constants?.appOwnership === 'expo';
} catch {
return false;
}
}

function hasVisionCamera(): boolean {
try {
const vc = require('react-native-vision-camera');
return !!vc;
} catch {
return false;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Optional Expo detection breaks Metro builds

Lines 111‑123 call require('expo-constants'). Metro resolves static requires at bundle time, so any consumer without Expo will now hit “Unable to resolve module 'expo-constants'” even though the call sits inside a try/catch. Please switch to a detection strategy that doesn’t require the package (e.g., inspect NativeModules.ExponentConstants or gate the require via a lazy globalThis.__r?. hook) so non-Expo apps keep building.

🤖 Prompt for AI Agents
In src/index.ts around lines 111 to 123, the optional Expo detection uses a
static require('expo-constants') which breaks Metro bundling; change the
detection to avoid static requires by checking runtime-native exports instead
(e.g., inspect NativeModules.ExponentConstants or use a guarded dynamic require
via globalThis.__r?. or a conditional typeof globalThis.__r === 'function' ?
globalThis.__r('expo-constants') : undefined) so Metro does not try to resolve
the module at bundle time; update the hasExpo detection to read from
NativeModules or use the guarded dynamic require and return the same boolean
result, keeping the try/catch logic only around the guarded access.

@gmemmy gmemmy merged commit 540946c into main Oct 10, 2025
2 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant