The display capability
Render UI on the Ray-Ban Display with the glasses.display Kotlin DSL — text, images, buttons, media, and Neural Band input. Gated per device; Android-first.
The display capability (glasses.display.*) renders UI on a glasses screen — today, the Ray-Ban Display. You compose a node tree with a small Kotlin builder DSL and hand it to glasses.display.show { ... }; the connected device renders it. On real hardware the tree is delivered over Meta DAT's mwdat-display path; in the browser simulator the same tree renders in the browser tab. There is no WebView, no canvas, no local-bitmap drawing — you declare a tree of containers and leaves, and the platform renders it.
Android-first; iOS port pending; real-hardware rendering being validated. The display surface is shipped and sim-verified on Android. The iOS
DisplayClientdoes not exist yet (iOS apps seeisAvailable == falseand should keep a voice-first path). The DSL, input routing, and event log are exercised end-to-end in the simulator; real-hardware rendering and the mid-pinch back gesture are being validated against DAT now — this page says so honestly rather than claiming a closed loop.
Per-device, never-throw
Display is a capability of the specific device model, not the vendor. Within Meta Ray-Ban, the Ray-Ban Display has a screen and the Ray-Ban Meta / Oakley models do not. So you branch on a runtime signal, never on a model name:
val isAvailable: Boolean = glasses.display.isAvailable // true only on a display-capable device
if (glasses.display.isAvailable) {
glasses.display.show { /* ... rich display UX ... */ }
} else {
glasses.audio.speak("Here's what I found …") // graceful voice fallback
}The model is never-throw: show() on a display-less device is a safe silent no-op — degradation never crashes and never diverges between the simulator and hardware. The gate is live: in the simulator, switching the device model pushes the new profile to the running app with no reconnect, so the same session can see isAvailable flip from true to false mid-run. Good UX guards rich display features with isAvailable and falls back to speech.
The builder DSL
show(onBack) { ... } runs against a DisplayRootScope. You nest containers and place leaves; show {} replaces the entire display each call (there are no incremental updates — own your view state app-side and re-render whole views on every transition). show() and clear() are suspend.
Containers:
column(...) { ... }— a vertical stack.row(...) { ... }— a horizontal stack.
Both take gap, padding, mainAlign, crossAlign, background, and an optional onClick that makes the whole container tappable. A clickable container needs an explicit id for a stable, agent-targetable handle — otherwise it falls back to a positional id (box-0, box-1, …) that shifts when you reorder the tree.
Leaves:
text(text, style, color, align)image(url, size, cornerRadius, align)— a remote image fetched by URL.button(text, style, icon, align, id, onClick)— pass an explicitid.icon(name, style, align)— a vendor-specific icon by name.
Root-only, full-surface (these render as the entire display and must be the sole top-level node — nesting them inside a column / row is a compile error, and the builder scope inside a container simply doesn't expose them):
video(url)— a remote MP4 by URL.video(clip)— a locally-recordedVideoClip(the SDK hosts it; see below).image(photo)— a locally-capturedPhotorendered full-surface (the SDK hosts it).
glasses.display.show(onBack = { scope.launch { glasses.display.clear() } }) {
column(gap = 8, padding = 12) {
text("My notes", style = TextStyle.HEADING)
column(
background = Background.CARD,
onClick = { scope.launch { openNote(note) } },
id = "note-${'$'}{note.id}", // stable identity-based id
) {
text(note.title, style = TextStyle.BODY)
text(note.preview, style = TextStyle.CAPTION, color = TextColor.SECONDARY)
}
}
}Enum vocabulary
| Enum | Cases |
|---|---|
TextStyle | HEADING, BODY, CAPTION |
TextColor | PRIMARY, SECONDARY |
ButtonStyle | PRIMARY, SECONDARY, OUTLINE |
Background | NONE, CARD |
Alignment | START, CENTER, END, STRETCH |
ImageSize | ICON, FILL |
CornerRadius | NONE, SMALL, MEDIUM |
Additive-light design rule
The Ray-Ban Display is a waveguide that adds light to the world — it can only brighten, never occlude. Dark pixels render transparent. So design bright-on-dark: bright text and strokes read well; dark fills disappear into the scene. The same applies double to video: bright-on-dark footage reads, dark scenes mostly vanish. The simulator composites the display additively (a screen blend) so previews match the optics.
One consequence to know: a dead or unfetchable media URL emits no light, so the panel shows nothing even while the tree-level checks look green — the one failure additive optics can't surface visually. See Errors and observability.
Full-surface video and photos, with hosting
video(...) is the one true-motion channel third-party content has — declarative tree updates are link-bound (a few Hz), but a root video plays on the glasses themselves. Playback auto-starts once the tree is sent and stops on the next show() / clear().
The glasses can only fetch an http(s) URL — a local data: / file: clip is rejected on hardware. So:
video(url)/image(url)— for media already hosted at anhttp(s)URL.video(clip)/image(photo)— for a clip or photo you just captured. The SDK uploads it to a copy on Extentos's media host atshow()time and renders the hosted URL; you never hand-roll hosting. A photo hosts in well under a second; a multi-MB clip takes a few seconds (you'll see a brief "Preparing…" only for video).
// Show a just-captured photo full-surface — the SDK hosts the local capture.
glasses.display.show(onBack = { scope.launch { showList() } }) {
image(photo)
}The hosted copy intentionally outlives the on-screen display, so re-showing the same clip/photo reuses it instead of re-uploading. Release it when the user deletes the underlying item:
forgetHostedVideo(clip)/forgetHostedImage(photo)— tear down the hosted copy. Idempotent and safe (a no-op if never hosted, already forgotten, or no display-video delivery is wired); does not touch the local file.prepareVideo(clip): Boolean— speculatively pre-upload a clip (e.g. when you render a list of recordings) so a latershow { video(clip) }plays instantly. Best-effort, idempotent, uploads at most once. There's noprepareImagesibling — photos host fast enough that there's no latency to move off the critical path.
Build the Photo / VideoClip the same way for show and for forget, so both resolve to the same hosted copy. Use platform-hosted test clips for quick verification: https://extentos.com/sample-videos/mountain-bike.mp4 and https://extentos.com/sample-videos/run-in-city.mp4.
Input model
The display has two input rails, both driven the same way in the simulator (injectInput) as on hardware:
- Selection — a Neural Band index-pinch on hardware (
injectInput(action: "select", targetId)in the sim) fires the node's registeredonClick, routed by nodeid. Give every clickable node a stable id. - Back — the back gesture (Neural Band mid-pinch on hardware;
injectInput(action: "back")in the sim) fires the current show'sonBack. Back is view-contextual, so register it pershow: a detail view'sonBacktypically re-renders the browse view; a root view'sonBackclears the display; ashowwith noonBackignores the gesture.clear()drops the current handlers so a staleonBackcan't fire after a wipe.
onClick / onBack are plain () -> Unit — launch a coroutine inside them for the suspend show() / clear() calls. The slide (focus highlight) moves device/sim-side; your app doesn't see focus changes, but the focus target is observable in the event log. Keep an explicit Back button alongside onBack for discoverability. The back-gesture wiring is sim-verified; the real-hardware mid-pinch lands with the DAT display validation pass.
Agent-driven verification reads the live tree with getDisplayState and drives input with injectInput — see the MCP tools reference.
Permissions
The display capability contributes zero extra Android permissions and no iOS plist keys — rendering is outbound over the existing DAT connection (the standard Bluetooth set every glasses app already declares). If your app already connects to the glasses, you can render to the display.
Errors and observability
There is no DisplayError return type. show() never throws; display delivery problems surface as runtime log events on glasses.runtime.events — display.video_delivery_failed, display.image_delivery_failed, display.video_error — and on the display chip in the simulator event log. A dead media URL additionally raises a visible "video failed to load" placeholder in the simulator viewport and a display_error entry on the errors chip — when a display flow looks green but shows nothing, check that chip first. App code can react to display.video_error (e.g. fall back to a text view). See the error reference.
Related
- Render on the display — the task guide: capture → store → browse, plus the assistant-driven variant
- Capabilities — the full vendor-agnostic SDK vocabulary and the per-device capability model
- The assistant runtime — assistant tools can drive
glasses.display.*by voice - Error reference — the no-
DisplayErrormodel and the runtime log events - Vendors: Meta Ray-Ban — the device family and the Ray-Ban Display
Related
Render on the display
Render UI on the Ray-Ban Display from your smart-glasses app. Capture a photo, save it to your own store, render a browsable list of cards with the glasses.display DSL, and show one full-surface on select — driven by direct calls with no LLM, or by assistant tools. Gate every call on glasses.display.isAvailable so display-less devices fall back gracefully. Android-first; iOS port pending.
Capabilities
The Extentos capability vocabulary — the vendor-agnostic SDK primitives (audio, camera, voice, assistant, display, hardware events) your handler subscribes to.
The assistant runtime
Build a voice assistant on smart glasses with glasses.assistant — wake/sleep, tools, vision, barge-in, memory, on the managed AI gateway. Phase-4 preview.
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.
Meta Ray-Ban (Meta DAT)
Complete Meta Ray-Ban developer guide — hardware tiers (Ray-Ban Meta, Ray-Ban Meta Gen 2, Ray-Ban Meta Display), the Meta Device Access Toolkit (DAT) public capability matrix, distribution and development state, what you can build and ship today vs what's still gated behind Meta partnerships, and how Extentos abstracts the toolkit so the same code runs against the simulator and real glasses.
The assistant runtime
Build a voice assistant on smart glasses with glasses.assistant — wake/sleep, tools, vision, barge-in, memory, on the managed AI gateway. Phase-4 preview.
Sessions
How an Extentos session works — the glasses connection-state machine, what persists across backgrounding and reconnects, and the three browser-simulator roles.