Capture video
Record a bounded video clip from the Meta Ray-Ban camera with glasses.camera.captureVideo(), stop it cleanly with stopVideo(), or subscribe to the continuous videoFrames stream for on-device ML and live preview.
Camera needs the Meta vendor module. com.extentos:glasses carries no
vendor SDK, so add implementation("com.extentos:glasses-meta") alongside it —
see install. Without it your build still succeeds
and voice still works, but capabilities.camera is false and every capture
returns an error. The SDK logs a warning at startup when it spots that
combination.
Two camera surfaces give you motion: glasses.camera.captureVideo() records a phone-side-encoded clip and returns a VideoClip, and glasses.camera.videoFrames(...) is a continuous Flow of camera frames for on-device ML or live preview. This page covers both, the clean stop pattern, and the cross-platform save gotcha.
Kotlin leads the snippets here and is the source of truth. The iOS
VideoConfigmatches it (includeAudiodefaultstrue,frameRatedefaults24); the gap is the bundled helpers — there are noVideos.*helpers on iOS. See the inline notes.
Record a clip
import com.extentos.glasses.core.VideoConfig
import com.extentos.glasses.core.VideoFormat
import com.extentos.glasses.core.Videos
import com.extentos.glasses.core.valueOrNull
import java.io.File
suspend fun record(glasses: ExtentosGlasses, context: Context) {
val clip = glasses.camera.captureVideo(
VideoConfig(
maxDurationSeconds = 60, // Int? — null (the default) means no time cap
includeAudio = true, // forces the HFP route while capturing (see below)
format = VideoFormat.MP4_HEVC, // MP4_HEVC | MP4_H264 | MOV — prefer HEVC
),
).valueOrNull() ?: return // ExtentosResult.Err — handle the same CaptureError variants as photo
val uri = clip.uri ?: return // VideoClip.uri is String? (nullable, transport-shaped)
val dst = File(context.filesDir, "clip-${System.currentTimeMillis()}.mp4")
if (!Videos.copyToFile(uri, dst)) return // save to your library dir
}The Kotlin VideoConfig has five fields: maxDurationSeconds: Int? (null = no cap), includeAudio: Boolean = true, format: VideoFormat = MP4_HEVC, resolution: Resolution = MEDIUM, and frameRate: Int = 24. The resolution / frameRate pair takes effect only when the recording is the first camera use of the session — video and videoFrames share one warm camera stream that can't be reconfigured live. There is no stopConditions field — older spec metadata advertised one, but it was never wired; the stop primitive is stopVideo() (below).
captureVideo() returns ExtentosResult<VideoClip, CaptureError> — the failure variants are the same as photo capture (DisabledByUser carries the gating toggle, CoexistenceBlocked on a camera conflict, etc. — see the error reference). Like Photo.uri, VideoClip.uri is a nullable String? whose scheme is data: in the simulator and file: on hardware; the Videos helpers (copyToFile, loadBytes, loadBase64, mediaTypeFromUri) bridge both — and like Photos, they never throw. Videos is Android-only today (iOS parity queued); on iOS read clip.uri (a String?) via URL(string:) + Data(contentsOf:) for file://, or parse the data: scheme by hand.
Codec: treat format as a label, not a guarantee
On Android today the clip is written by the phone-side encoder as H.264 in an MP4 container — on real glasses and in the simulator alike — regardless of the VideoFormat you request; the VideoClip.format field there is a container hint, not a codec promise. On iOS the writer honors the requested format (MP4_H264 → H.264, otherwise HEVC). Dev/prod parity is on the clip shape.
includeAudio forces HFP coexistence
When includeAudio = true, audio captures over the glasses-as-mic Bluetooth HFP profile, which suspends A2DP playback for the duration — any music drops from hi-fi A2DP to voice-band HFP mono until the clip finishes. If your app plays music while recording, set includeAudio = false — it defaults to true on both platforms, so opt out explicitly. See audio streaming for the coexistence model.
Stop a recording cleanly
A clip with maxDurationSeconds = null runs until you stop it. The clean primitive is glasses.camera.stopVideo() — the in-flight captureVideo() then resumes with Ok holding the partial clip recorded up to that point. No coroutine cancellation, no exception, no separate result channel:
val capture = scope.async { glasses.camera.captureVideo(VideoConfig(includeAudio = true)) }
// ...later, on the user's "stop" intent:
glasses.camera.stopVideo()
val clip = capture.await().valueOrNull() // natural await — returns the partial clipstopVideo() is idempotent (a no-op when nothing is recording).
Don't cancel the wrapping
asyncDeferred to stop a recording. Once a Deferred is cancelled its state is sticky-Cancelled andawait()throws even though the body returned a value — you'd lose the clip. UsestopVideo().
The voice-stop pattern and NonCancellable
A common shape is a wake phrase that starts recording and a stops phrase that ends it. When you stop via the stops-cancellation path (rather than stopVideo()), the library still recovers the partial: it sends an abort to the transport and drains the partial video_result for up to 10 s under NonCancellable, so captureVideo() returns Ok(VideoClip) instead of throwing. But the handler coroutine has already been cancellation-requested by the time the clip comes back — so your save I/O must run under withContext(NonCancellable) { ... } or it throws CancellationException and silently drops the clip:
glasses.voice.onPhrase(
phrase = "start recording",
stops = listOf("stop recording"),
) {
val clip = glasses.camera.captureVideo(
VideoConfig(maxDurationSeconds = 60, includeAudio = true),
).valueOrNull() ?: return@onPhrase
val uri = clip.uri ?: return@onPhrase
// Post-stop I/O MUST survive the surrounding handler cancellation:
withContext(NonCancellable) {
Videos.copyToFile(uri, dst)
repo.insertRecording(/* ... */)
}
}On iOS the equivalent is running the post-stop save in a Task.detached. The drain ceiling is 10 s — real-hardware MediaRecorder.stop → encode can take 3–5 s on a 20-second clip. If the encode exceeds 10 s (very large clips / slow devices) the original CancellationException rethrows and the partial is lost; bound your clip lengths, or use a videoFrames + MediaMuxer custom encoder for clips over ~30 s.
Continuous frame stream
For anything that needs more than one frame per second — on-device object detection / scene classification, a live preview — subscribe to videoFrames:
import com.extentos.glasses.core.Resolution
import com.extentos.glasses.core.VideoFrameConfig
glasses.camera.videoFrames(
VideoFrameConfig(
resolution = Resolution.LOW, // LOW is plenty for most ML
frameRate = 2, // 2 fps for ML; 7–15 for preview
),
).collect { frame ->
// frame.data is encoded image bytes (the simulator sends JPEG —
// BitmapFactory.decodeByteArray before use). Also frame.width / height / timestampMs.
}The Kotlin VideoFrameConfig is intentionally minimal — resolution and frameRate only. (iOS additionally exposes codec and backpressure; Kotlin handles backpressure internally.) Frame rate drives battery hard: 2 fps for ML drains negligibly, 7–15 fps for preview lasts ~1 hr, 24–30 fps drains in ~30 min. Pair any high-fps stream with a thermal listener that downgrades it (see hardware events). battery_save_mode = true clamps the stream to LOW + 2 fps regardless of config and emits a camera_stream_clamped event so the downgrade is observable. videoFrames (and captureVideo) require FOREGROUND_SERVICE on Android 14+ — see getPermissions.
If the camera is paused when you start
The wearer can pause the camera with the glasses' capture button — a single temple tap (tap again to resume; tap-and-hold stops). If you start videoFrames while it's paused, the flow throws CameraStreamPausedException at collection — the streaming analogue of the CaptureError.StreamPaused that capturePhoto/captureVideo return. Catch it and prompt the wearer to tap the temple, then start again:
import com.extentos.glasses.core.CameraStreamPausedException
try {
glasses.camera.videoFrames(config).collect { render(it) }
} catch (e: CameraStreamPausedException) {
glasses.audio.speak("The camera is paused — tap the right temple to resume.")
}A pause that happens mid-stream is not an error — the frames simply stop (your last-rendered frame freezes on screen) and resume when the wearer taps. Meta's DAT has no app-callable resume, so never try to force it with a teardown-and-reopen; that drops the whole connection.
A pause during a recording skips footage
A captureVideo recording is made of the stream, so a mid-recording temple tap doesn't end it — the recording stays alive but captures no footage while paused. End it while paused and the clip stops at the pause point; let the wearer resume and keep recording, and the final clip contains everything except the paused window, spliced seamlessly. The clip's duration_ms reflects footage, not wall clock. A tap-and-hold (stop) while recording ends it with whatever footage was captured so far. The simulator reproduces all of this — drive the capture button from the sim page's Hardware-buttons panel or injectHardwareButton and watch the REC pill flip to "paused — no footage".
Verify it in the simulator
captureVideo() emits a capture_video_returned event when it resolves — getEventLog(filter: "all") shows (the *_returned row classifies under the custom chip) success, duration_ms, clip_duration_ms, and via. The via value tells you which path produced the clip: cancelled_with_partial means a stop fired and the drain recovered a partial; cancelled means the drain produced nothing. The simulator is designed to behave like the glasses, but codec and HFP timing are substrate deltas under active validation — confirm long-clip and music-coexistence behavior on real hardware.
Related
- Capture a photo — one-shot stills, the
CaptureErrorvariants, and the URI-helper model the snippets above reuse. - Audio streaming — the A2DP/HFP coexistence that
includeAudiotriggers. - Voice triggers — the
onPhrase+stopsshape used for voice-controlled recording. - Error reference — every
CaptureErrorvariant. getCapabilityGuide(feature: "capture_video")/getCapabilityGuide(feature: "video_frames")— minimal call shapes + full gotchas.
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.
The capture button
The touch surface on the Meta Ray-Ban's right temple can pause or stop your camera stream at any moment — a firmware-enforced privacy gesture your app must respect. What your app observes, how to handle it, and how to test it in the simulator.