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
capturePhotosurface with fewer bundled helpers (onlyphoto.loadImage()). See Capture a photo.
Quick checklist (verify these first)
- The Meta vendor module is on your classpath. Camera lives in
com.extentos:glasses-meta, not incom.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 withglasses.transportChosen.REAL_METAmeans the vendor arm claimed;SYSTEM_AUDIOmeans 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 thatBLUETOOTH_CONNECTwasn't granted beforeExtentosGlasses.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). CAMERAis declared and granted. The library does not declareCAMERAfor 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 dedicatedPermissionDenied. See Android manifest setup.- The glasses are connected (
Active) and report a camera. A capture before the connection reachesActive, or after it drops, returnsCaptureError.NotConnected. Gate onglasses.connection.statetogether withglasses.capabilities.camera— on the vendorless audio baseline the state isActive(the phone always has audio) while there is no camera at all, and capture there returnsCaptureError.PlatformError(code = "system_audio_no_camera")rather thanNotConnected. - No camera toggle is off. If the user (or your code) closed
camera_streaming_enabled, capture returnsCaptureError.DisabledByUser(toggle = "camera_streaming_enabled"). The same applies underprivacy_mode, which kills all capture. Readglasses.toggles.stateand surface the gate to the user. - Nothing is racing the audio profile. Another capture already in progress — most often an open
videoFrames()stream, or a rapid second capture — can surfaceCaptureError.CoexistenceBlocked. See below. - Read the event log.
getEventLog(filter: 'camera')shows the capture request and camera-layer events (the result eventcapture_photo_returnedlands on thecustomchip, orerrorson failure);getEventLog(filter: 'errors')surfaces the typed failure (severity≥warn lands on theerrorschip 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. |
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. |
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_policytoggle (prefer_videoby 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.
Related
- Error reference — the full
CaptureErrorpayload shapes. - Capture a photo — the call shape and the photo → vision-LLM flow.
- Capabilities — camera primitives, the Photos helpers, and the toggle gates.
- Glasses won't connect — fixing the
NotConnectedroot cause.
Related
Error reference
Every typed error the Extentos SDK can return — ConnectError, CaptureError, AudioError, TransportError, the ExtentosError umbrella, and the Meta-DAT DeviceSessionError — with their payload fields and meaning. Lifecycle operations return ExtentosResult<T, E> with these concrete failure variants rather than throwing; pattern-match them. Generated from the Rust core.
Capture a photo
Take a still photo on the Meta Ray-Ban camera from your handler code, handle CaptureError cleanly, and pipe the image into a vision LLM. Uses glasses.camera.capturePhoto() and the Photos URI helpers.
Glasses won't connect
Fix common first-connection failures on Meta Ray-Ban — no pairing dialog, session won't start, the DAT auth callback never fires, or an immediate disconnect.
Wake phrase not matching
You subscribed to glasses.audio.transcriptions() and matched a wake phrase, but the handler doesn't run when you say it. Common causes and fixes.
Background session killed
Fix a Meta Ray-Ban session that dies when your app backgrounds — a missing Android foreground service, missing iOS background modes, or an OS low-memory kill.