---
title: Photo capture fails
description: Fix capturePhoto() returning an error or no image on Meta Ray-Ban — CAMERA permission, thermal-critical, folded hinges, coexistence, and the camera toggle.
type: troubleshooting
platform: all
vendor: meta
related:
  - /docs/reference/errors
  - /docs/guides/photo-capture
  - /docs/troubleshooting/glasses-wont-connect
---

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`](/docs/reference/errors#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](/docs/guides/photo-capture).

## 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](/docs/sdk/android/manifest#what-you-add).
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:

| Variant | Cause | Fix |
|---|---|---|
| `PermissionDenied` | Declared, but the capture path doesn't emit it today — a missing `CAMERA` grant surfaces as `PlatformError` | Still declare `CAMERA` + request it at runtime; handle a missing grant via the `PlatformError` branch. |
| `NotConnected` | The glasses connection isn't `Active` | Gate capture on `glasses.connection.state`; reconnect first. See [glasses won't connect](/docs/troubleshooting/glasses-wont-connect). |
| `ThermalCritical` | Declared, not currently returned on the capture path — a thermal drop surfaces as `NotConnected` | Handle for forward-compat; treat like `NotConnected` today. |
| `HingesClosed` | Declared, not currently returned on the capture path — a fold surfaces as `NotConnected` | Handle for forward-compat; treat like `NotConnected` today. |
| `CoexistenceBlocked` | Another capture is already in progress (e.g. an open `videoFrames()` stream, or a rapid second capture) | See the section below. |
| `DisabledByUser` | A 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. |
| `PlatformError` | A native-origin capture failure — structured `code` + `message` | Log `code`/`message`; inspect the event log entry at the catch site for the underlying native exception. |

```kotlin
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](/docs/concepts/capabilities#toggles--runtime-gates-the-user-controls) toggle table and [audio streaming](/docs/guides/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](/docs/concepts/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.

## Related

- [Error reference](/docs/reference/errors#captureerror) — the full `CaptureError` payload shapes.
- [Capture a photo](/docs/guides/photo-capture) — the call shape and the photo → vision-LLM flow.
- [Capabilities](/docs/concepts/capabilities) — camera primitives, the Photos helpers, and the toggle gates.
- [Glasses won't connect](/docs/troubleshooting/glasses-wont-connect) — fixing the `NotConnected` root cause.
