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.
Related
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.
Audio streaming
Stream raw mic audio off Meta Ray-Ban with audioChunks, get continuous STT with transcriptions(), and play TTS with speak(). Covers A2DP/HFP coexistence.
Voice triggers
Wire a voice command on the glasses to an action in your app. Works on Meta Ray-Ban via the phone's speech recognizer over Bluetooth. Phrases auto-surface on the connection page and the simulator's click-to-fire panel.
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.
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.