Troubleshooting

Photo capture fails

Fix capturePhoto() returning an error or no image on Meta Ray-Ban — CAMERA permission, thermal-critical, folded hinges, coexistence, and the camera toggle.

You call glasses.camera.capturePhoto() and get back ExtentosResult.Err instead of a Photo — or a result you can't render. Capture failures are a typed CaptureError; pattern-match the result (don't wrap it in runCatching — it won't catch a returned error). This page maps each CaptureError variant to its cause and fix.

Kotlin leads the examples and is the source of truth; iOS has the same capturePhoto surface with fewer bundled helpers (only photo.loadImage()). See Capture a photo.

Quick checklist (verify these first)

  1. The Meta vendor module is on your classpath. Camera lives in com.extentos:glasses-meta, not in com.extentos:glasses. If capture worked before an SDK upgrade and now fails, this is the first thing to check — the build succeeds without it and only capture breaks. Confirm with glasses.transportChosen. REAL_META means the vendor arm claimed; SYSTEM_AUDIO means it didn't — and the missing module is only one of three causes. The other two are that no glasses are bonded to the phone, and that BLUETOOTH_CONNECT wasn't granted before ExtentosGlasses.create(...) (the vendor arm reads the bonded list, so an ungranted permission looks identical to no glasses). If the dependency is present, check those two before re-adding it. The SDK also logs a warning at startup (adb logcat -s Extentos:W).
  2. CAMERA is declared and granted. The library does not declare CAMERA for you — it's a capability permission your app owns. Add <uses-permission android:name="android.permission.CAMERA" /> and request it at runtime. A missing grant surfaces as a capture failure (PlatformError) — the capture path has no dedicated PermissionDenied. See Android manifest setup.
  3. The glasses are connected (Active) and report a camera. A capture before the connection reaches Active, or after it drops, returns CaptureError.NotConnected. Gate on glasses.connection.state together with glasses.capabilities.camera — on the vendorless audio baseline the state is Active (the phone always has audio) while there is no camera at all, and capture there returns CaptureError.PlatformError(code = "system_audio_no_camera") rather than NotConnected.
  4. No camera toggle is off. If the user (or your code) closed camera_streaming_enabled, capture returns CaptureError.DisabledByUser(toggle = "camera_streaming_enabled"). The same applies under privacy_mode, which kills all capture. Read glasses.toggles.state and surface the gate to the user.
  5. Nothing is racing the audio profile. Another capture already in progress — most often an open videoFrames() stream, or a rapid second capture — can surface CaptureError.CoexistenceBlocked. See below.
  6. Read the event log. getEventLog(filter: 'camera') shows the capture request and camera-layer events (the result event capture_photo_returned lands on the custom chip, or errors on failure); getEventLog(filter: 'errors') surfaces the typed failure (severity≥warn lands on the errors chip regardless of modality).

Capture call returns an error

Branch on the CaptureError variant — each has a distinct fix:

VariantCauseFix
PermissionDeniedDeclared, but the capture path doesn't emit it today — a missing CAMERA grant surfaces as PlatformErrorStill declare CAMERA + request it at runtime; handle a missing grant via the PlatformError branch.
NotConnectedThe glasses connection isn't ActiveGate capture on glasses.connection.state; reconnect first. See glasses won't connect.
ThermalCriticalDeclared, not currently returned on the capture path — a thermal drop surfaces as NotConnectedHandle for forward-compat; treat like NotConnected today.
HingesClosedDeclared, not currently returned on the capture path — a fold surfaces as NotConnectedHandle for forward-compat; treat like NotConnected today.
CoexistenceBlockedAnother capture is already in progress (e.g. an open videoFrames() stream, or a rapid second capture)See the section below.
DisabledByUserA user-facing toggle gates capture (e.toggle names which — e.g. camera_streaming_enabled, or privacy_mode)Explain the toggle to the user; ask them to re-enable it via the connection page.
PlatformErrorA native-origin capture failure — structured code + messageLog code/message; inspect the event log entry at the catch site for the underlying native exception.
when (val r = glasses.camera.capturePhoto()) {
    is ExtentosResult.Ok  -> render(r.value)
    is ExtentosResult.Err -> when (val e = r.error) {
        is CaptureError.PermissionDenied   -> requestCameraPermission()
        is CaptureError.NotConnected       -> promptReconnect()
        is CaptureError.ThermalCritical    -> toast("Glasses too hot — let them cool")
        is CaptureError.HingesClosed       -> toast("Open the glasses to take a photo")
        is CaptureError.CoexistenceBlocked -> toast("Busy — try again in a moment")
        is CaptureError.StreamPaused       -> toast("Tap the right temple to resume the camera")
        is CaptureError.DisabledByUser     -> explainToggle(e.toggle)
        is CaptureError.PlatformError      -> log(e.code, e.message)
    }
}

CoexistenceBlocked — another capture is in progress

The DAT camera path returns CaptureError.CoexistenceBlocked when another capture is already in progress — most commonly a capturePhoto() fired while a videoFrames() stream is open, or two captures racing. (Meta Ray-Ban's Bluetooth audio path — HFP/SCO for capture, A2DP for playback — also coexists fragilely with the camera, so sequencing audio and capture remains good practice.) Fixes:

  • Don't hold an open videoFrames() stream while doing a one-shot photo unless you need both — stop the stream first.
  • Sequence audio and capture rather than overlapping them: finish speak() / recordDiscrete(), then capture.
  • The audio_video_coexistence_policy toggle (prefer_video by default) is a declared preference the library does not yet enforce — sequence audio and capture in your handler yourself. See the capabilities toggle table and audio streaming for the full coexistence story.

Capture succeeds but the image is empty or won't load

You got an Ok Photo, but the bytes are missing or the image won't decode. This is almost always a URI-scheme mismatch — the photo URI differs across transports (a data: URI in the simulator vs a file: URI on hardware):

  • Use the Photos URI helpers rather than reading the URI yourself: Photos.loadBytes(uri), Photos.loadBase64(uri), Photos.loadBitmap(uri) on Android (photo.loadImage() on iOS). They normalize the scheme variance so the same handler runs on BrowserSim, LocalSim, and real Meta Ray-Ban. See capabilities § Camera primitives.
  • Don't hardcode an assumption about the URI scheme — what works in the simulator may not match what real hardware returns.

Sim ≠ hardware. A photo that captures and renders perfectly in the browser simulator (webcam-sourced) does not guarantee the same on real glasses — camera fidelity, framing, and the URI scheme differ. Verify capture on real hardware before relying on it.