# AGENTS.MD – Meta Wearables Device Access Toolkit (DAT) iOS 0.2
> Living spec + implementation guide for building iOS apps with the Meta Wearables Device Access Toolkit (DAT) targeting Ray-Ban Meta / Oakley Meta smart glasses.
---
## 0. Goals of this document
This file is meant to be the **single source of truth** for how we integrate the Meta Wearables Device Access Toolkit in our iOS codebase.
It should help any AI agent / human developer to:
1. Understand the **capabilities and limits** of the SDK.
2. Follow a **consistent architecture** (registration → permissions → device selection → sessions → streaming / photo / audio).
3. Reuse **common patterns** (session management, error handling, device selectors, photo capture, etc.).
4. Quickly find **API details** (types, enums, methods) without re-opening the docs.
Whenever the SDK changes, **update this file first**.
---
## 1. High-level overview
### 1.1 What DAT does
The Wearables Device Access Toolkit (DAT) lets an iOS app connect to supported Meta smart glasses and use them as **remote sensors & outputs**:
- **Camera**: live video streaming + still photo capture.
- **Microphones**: voice input via glasses mic over Bluetooth.
- **Speakers**: audio playback through glasses speakers.
All application logic runs on the **phone**. Glasses provide sensor data and audio I/O; there is **no app runtime on the glasses**.
### 1.2 Supported devices (currently)
- Ray-Ban Meta (Gen 1)
- Ray-Ban Meta (Gen 2)
- Oakley Meta HSTN
Future devices may be added; the SDK exposes `DeviceType` / compatibility utilities.
### 1.3 Core concepts
- **Registration**: one-time handshake between our app and the Meta AI companion app. After registration, our app appears in the Meta AI "App Connections" list.
- **Permissions**: camera permission is granted per-app but confirmed per-device through Meta AI. Microphone/speaker access is handled via iOS audio + Bluetooth permissions.
- **Devices**: AI glasses linked to the Meta AI app and visible through DAT.
- **Device selector**: object deciding **which device** a given session targets (auto or specific).
- **Sessions**:
- **StreamSession**: media session for live video + optional photo capture.
- **DeviceStateSession**: session focused on tracking device state.
- **Publishers / async streams**: provide state updates, frames, errors, photo data, registration state, device lists.
---
## 2. iOS integration checklist
### 2.1 Prerequisites
- Xcode project with a **registered bundle identifier**.
- Access to Meta Wearables DAT GitHub repo (`meta-wearables-dat-ios`).
- Meta AI companion app installed on device and compatible glasses paired.
### 2.2 Info.plist configuration
We must configure:
1. **Custom URL scheme** for callbacks from Meta AI.
2. **Query scheme** to allow `fb-viewapp`.
3. **External accessory protocol** for Meta wearables.
4. **Background modes** for Bluetooth / external accessory.
5. **Bluetooth usage description**.
6. **MWDAT dictionary** for `AppLinkURLScheme` + `MetaAppID`.
Template (adjust `myexampleapp` and `MetaAppID`):
```xml
CFBundleURLTypes
CFBundleTypeRole
Editor
CFBundleURLName
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleURLSchemes
myexampleapp
LSApplicationQueriesSchemes
fb-viewapp
UISupportedExternalAccessoryProtocols
com.meta.ar.wearable
UIBackgroundModes
bluetooth-peripheral
external-accessory
NSBluetoothAlwaysUsageDescription
Needed to connect to Meta Wearables
MWDAT
AppLinkURLScheme
myexampleapp://
MetaAppID
0
```
> If Info.plist is preprocessed, the `://` suffix may get stripped unless the `-traditional-cpp` flag is used (Apple TN2175).
### 2.3 Add SDK via Swift Package Manager
1. Xcode → **File > Add Package Dependencies…**
2. URL: `https://github.com/facebook/meta-wearables-dat-ios/`
3. Pick a tagged release (e.g. `v0.2.0.x`).
4. Add package to all targets that require wearables access.
5. In Swift files using the SDK, import the specific modules:
```swift
import MWDATCore // Core features, registration, device discovery
import MWDATCamera // Camera streaming, StreamSession
import MWDATMockDevice // (Debug only) Mock device support
```
### 2.4 Configure SDK on launch
Call **once** during app startup:
```swift
func configureWearables() {
do {
try Wearables.configure()
} catch {
assertionFailure("Failed to configure Wearables SDK: \(error)")
}
}
```
### 2.5 URL handling for Meta AI callbacks
When Meta AI returns to the app (registration, permissions), we must forward the URL to the SDK.
**SwiftUI (WindowGroup level):**
```swift
.onOpenURL { url in
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
components.queryItems?.contains(where: { $0.name == "metaWearablesAction" }) == true else {
return
}
Task {
do {
_ = try await Wearables.shared.handleUrl(url)
} catch {
print("Handle URL failed: \(error)")
}
}
}
```
**UIKit (AppDelegate / SceneDelegate):**
```swift
func handleWearablesCallback(url: URL) async throws {
_ = try await Wearables.shared.handleUrl(url)
}
```
---
## 3. Registration, devices & permissions
### 3.1 Registration
Registration binds our app to the user’s glasses via Meta AI:
- One-time per install / account.
- User is deep-linked to Meta AI to confirm, then sent back via callback URL.
- After registration, our app appears in Meta AI **App Connections** list.
- User can unregister in Meta AI; we can also start unregistration in-app.
API:
```swift
func startRegistration() throws {
try Wearables.shared.startRegistration()
}
func startUnregistration() throws {
try Wearables.shared.startUnregistration()
}
```
Observe registration state asynchronously:
```swift
let wearables = Wearables.shared
Task {
for await state in wearables.registrationStateStream() {
// Update registration UI (connected / not connected / in progress)
}
}
```
### 3.2 Device discovery
Devices (glasses) are discovered via `devicesStream()`:
```swift
Task {
for await devices in wearables.devicesStream() {
// devices: [Device]
// Update our device list UI / selection
}
}
```
`Device` represents a pair of glasses.
Key members:
- `identifier: DeviceIdentifier` — unique ID.
- `name: String` — human-readable device name (may be empty).
- `nameOrId() -> String` — name or identifier fallback.
- `deviceType() -> DeviceType` — e.g. Ray-Ban Meta.
- `compatibility() -> Compatibility` — compatibility with this DAT version.
- `linkState` — connection state.
- `addCompatibilityListener(_:)` — listen for compatibility changes.
- `addLinkStateListener(_:)` — listen for link state changes.
### 3.3 Permissions
**Camera permission** is managed by DAT through Meta AI:
- Granted at app level but confirmed per device.
- Flow is handled in Meta AI, with options like **Allow once** / **Allow always** / **Deny**.
API pattern:
```swift
var cameraStatus: PermissionStatus = .denied
cameraStatus = try await wearables.checkPermissionStatus(.camera)
if cameraStatus != .granted {
cameraStatus = try await wearables.requestPermission(.camera)
}
```
Notes:
- If user denies, we must handle gracefully (disable features, show explanation).
- With multiple devices, permission is **granted** if any linked device has approved.
**Microphone / speaker** access uses Bluetooth HFP/A2DP and is governed by iOS audio permissions and routing (not a DAT permission type). See audio section.
---
## 4. Device selection & state sessions
### 4.1 Device selectors
All session-based operations (e.g. `StreamSession`) use a **DeviceSelector**.
#### `AutoDeviceSelector`
```swift
final class AutoDeviceSelector: DeviceSelector {
public init(wearables: WearablesInterface)
var activeDevice: DeviceIdentifier?
public func activeDeviceStream() -> AnyAsyncSequence
}
```
Behavior:
- Automatically selects the “best” device.
- Picks the first connected device from the devices list.
- If no device is connected, falls back to the first device.
- `activeDeviceStream()` updates whenever the device list changes.
Use when we want **smart default selection** with minimal UI friction.
#### `SpecificDeviceSelector`
```swift
class SpecificDeviceSelector: DeviceSelector {
public init(device: DeviceIdentifier)
var activeDevice: DeviceIdentifier? { get }
public func activeDeviceStream() -> AnyAsyncSequence
}
```
Behavior:
- Always targets a **specific** device (by identifier).
- `activeDeviceStream()` yields that device and then completes.
Use when user explicitly selects a device in UI.
### 4.2 DeviceStateSession
`DeviceStateSession` monitors state changes for the selected device.
```swift
final class DeviceStateSession {
public init(deviceSelector: DeviceSelector)
var state: /* device state type – see full docs */
public func start()
public func stop()
}
```
Use this when we need ongoing device state monitoring separate from media streaming.
---
## 5. StreamSession – video streaming & photo capture
### 5.1 Overview
`StreamSession` manages a **media streaming session** from glasses:
- Live video frames from camera.
- On-demand still photo capture during streaming.
- Real-time state updates.
- Error reporting.
```swift
final class StreamSession {
// Constructors
public init(deviceSelector: DeviceSelector)
public init(streamSessionConfig: StreamSessionConfig,
deviceSelector: DeviceSelector)
// Properties
var state: StreamSessionState { get }
var streamSessionConfig: StreamSessionConfig { get }
var statePublisher: /* Publisher */ { get }
var videoFramePublisher: /* Publisher */ { get }
var photoDataPublisher: /* Publisher */ { get }
var errorPublisher: /* Publisher */ { get }
// Methods
public func start()
public func stop()
public func capturePhoto(format: PhotoCaptureFormat) -> Bool
}
```
New sessions start in `.stopped` state.
### 5.2 StreamSessionConfig
Configuration for a streaming session:
```swift
struct StreamSessionConfig {
var videoCodec: VideoCodec
var resolution: StreamingResolution
var frameRate: UInt
public init(videoCodec: VideoCodec,
resolution: StreamingResolution,
frameRate: UInt)
public init() // defaults: raw codec, medium resolution, 30 FPS
}
```
### 5.3 StreamingResolution
Valid 9:16 resolutions:
```swift
enum StreamingResolution: CaseIterable {
case high // 720 x 1280
case medium // ~504 x 896
case low // 360 x 640
var videoFrameSize: VideoFrameSize { get }
}
```
Internally the SDK may use additional steps (576x1024, 540x960, 432x768) for laddering.
### 5.4 VideoCodec
```swift
enum VideoCodec {
case raw // raw decompressed frames
}
```
Currently only `.raw` is exposed.
### 5.5 StreamSessionState
```swift
enum StreamSessionState {
case stopping
case stopped
case waitingForDevice
case starting
case streaming
case paused
}
```
Transitions:
- `.stopped → .waitingForDevice` (no compatible device at `start()` time).
- `.stopped → .starting → .streaming` (device available).
- Any → `.stopping → .stopped` (on `stop()` or error).
- `.streaming ↔ .paused` (device / system pauses and resumes session).
### 5.6 StreamSessionError
```swift
enum StreamSessionError: Error, Equatable {
case internalError
case deviceNotFound(DeviceIdentifier)
case deviceNotConnected(DeviceIdentifier)
case timeout
case videoStreamingError
case audioStreamingError
case permissionDenied
}
```
Emitted via `errorPublisher` on various failures (connection, permissions, timeouts, internal issues).
**User-Friendly Error Mapping Example:**
```swift
private func formatStreamingError(_ error: StreamSessionError) -> String {
switch error {
case .internalError:
return "An internal error occurred. Please try again."
case .deviceNotFound:
return "Device not found. Please ensure your device is connected."
case .deviceNotConnected:
return "Device not connected. Please check your connection and try again."
case .timeout:
return "The operation timed out. Please try again."
case .videoStreamingError:
return "Video streaming failed. Please try again."
case .audioStreamingError:
return "Audio streaming failed. Please try again."
case .permissionDenied:
return "Camera permission denied. Please grant permission in Settings."
@unknown default:
return "An unknown streaming error occurred."
}
}
```
### 5.7 VideoFrame & VideoFrameSize
```swift
struct VideoFrame: Sendable {
var sampleBuffer: CMSampleBuffer { get } // read-only
/// Converts to UIImage safely.
public func makeUIImage() -> sending UIImage?
}
struct VideoFrameSize {
public init(width: UInt, height: UInt)
var width: UInt
var height: UInt
}
```
**Important**: `sampleBuffer` is shared; do **not** mutate attachments, timing, or pixel buffer. Use `makeUIImage()` for safe conversion.
### 5.8 PhotoCaptureFormat & PhotoData
```swift
enum PhotoCaptureFormat: Sendable {
case heic
case jpeg
}
struct PhotoData: Sendable {
public init(data: Data, format: PhotoCaptureFormat)
let data: Data
let format: PhotoCaptureFormat
}
```
Usage with `StreamSession`:
- Call `capturePhoto(format:)` while streaming.
- If it returns `true`, subscribe to `photoDataPublisher` for `PhotoData`.
- Streaming pauses during capture and resumes automatically after.
### 5.9 Example: start stream & display frames
```swift
let wearables = Wearables.shared
let deviceSelector = AutoDeviceSelector(wearables: wearables)
let config = StreamSessionConfig(
videoCodec: .raw,
resolution: .low,
frameRate: 24
)
let session = StreamSession(
streamSessionConfig: config,
deviceSelector: deviceSelector
)
// Track loading state
var hasReceivedFirstFrame = false
let stateToken = session.statePublisher.listen { state in
Task { @MainActor in
// Update UI (e.g. show streaming / paused / stopped)
}
}
let frameToken = session.videoFramePublisher.listen { frame in
guard let image = frame.makeUIImage() else { return }
Task { @MainActor in
// Display the image in a preview surface
// Set hasReceivedFirstFrame = true
}
}
Task {
await session.start()
}
```
### 5.10 Example: photo capture
```swift
let accepted = session.capturePhoto(format: .jpeg)
if !accepted {
// No active session, capture already in progress, or capture failed
}
let photoToken = session.photoDataPublisher.listen { photoData in
let data = photoData.data
if let image = UIImage(data: data) {
// Save, share, or process the image
}
}
```
### 5.11 Bandwidth & quality laddering
- Streaming uses **Bluetooth Classic**.
- SDK automatically manages bandwidth:
- First lowers resolution (High → Medium → Low).
- Then reduces frame rate (e.g. 30 → 24 → 15 FPS, never below 15).
- Effective image quality may be lower than configured resolution because of adaptive per-frame compression.
- Requesting lower resolution and/or frame rate can yield better perceived quality with fewer artifacts.
---
## 6. Audio: microphones & speakers
### 6.1 Bluetooth profiles
Glasses audio uses:
- **A2DP** — high-quality, output-only audio (media).
- **HFP** — two-way audio (mic + speaker) for voice interactions.
DAT shares mic/speaker access with the system Bluetooth stack; we control it via `AVAudioSession`.
### 6.2 AVAudioSession configuration (iOS)
Typical setup for HFP:
```swift
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playAndRecord,
mode: .default,
options: [.allowBluetooth])
try audioSession.setActive(true,
options: .notifyOthersOnDeactivation)
```
Notes:
- Use `.playAndRecord` category for input + output.
- Combine with `.allowBluetooth` and/or `.allowBluetoothA2DP` as appropriate.
- Listen for `AVAudioSession.routeChangeNotification` to handle device connect/disconnect.
### 6.3 Coordinating audio with streaming
When using HFP and streaming at the same time:
- Ensure HFP audio session is fully configured **before** starting a `StreamSession` that uses audio.
- Avoid fixed-sleep hacks in real code; prefer state-based detection (route change events).
Pseudo:
```swift
func startStreamSessionWithAudio() async {
// 1. Set up HFP
startAudioSession()
// 2. Wait for audio route to be ready (replace with real logic)
try? await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
// 3. Start streaming
await streamSession.start()
}
```
### 6.4 Practical tips
- For voice-centric features, favor stable HFP routing; keep video resolution/FPS conservative.
- For visual-centric features, be aware that HFP can reduce audio quality; test UX carefully.
- Log route changes and `StreamSessionError.audioStreamingError` for diagnostics.
---
## 7. Session lifecycle & device behavior
### 7.1 Sessions vs transactions
- **Device sessions** (what we use via `StreamSession`): long-lived, sensor/output access.
- **Transactions**: short, system-owned interactions (notifications, “Hey Meta” triggers) – not controlled by us.
### 7.2 Device-driven state changes
Session state can change when:
- User opens another experience via system gesture.
- Another app or system component starts a device session.
- Glasses are removed, folded, or moved out of range → Bluetooth disconnect.
- User removes our app from Meta AI companion app.
- Meta AI ↔ glasses connectivity drops.
We **must not** guess the cause; just react to state (`StreamSessionState`, device link state, availability).
### 7.3 Device availability
- Hinge position and wear detection affect connectivity but are not exposed directly.
- Key observable effects:
- Closing hinges → Bluetooth disconnect, active streams stop, sessions go to `.stopped`.
- Opening hinges → Bluetooth may reconnect, but **sessions do not auto-restart**. We must explicitly start sessions once device is available and user initiates.
### 7.4 Lifecycle checklist
- Subscribe to **registrationStateStream** and **devicesStream**.
- Before starting streaming:
- Verify **registered**.
- Verify camera permission **granted**.
- Verify at least one compatible device **available**.
- React to `StreamSessionState`:
- `.streaming`: show active UI; process frames.
- `.paused`: keep connection; pause heavy processing; show paused UI.
- `.stopped`: release resources; show idle UI; allow restart.
- Handle `StreamSessionError` values (especially `.permissionDenied`, `.deviceNotConnected`, `.deviceNotFound`, `.timeout`).
---
## 8. Recommended architecture (for agents & humans)
Use an **agent-based modular architecture** so both humans and AI tools can work safely.
### 8.1 Modules / agents
1. **WearablesBootstrapAgent**
- Calls `Wearables.configure()` at startup.
- Checks environment (Info.plist, MetaAppID).
- Sets global flags (dev mode vs production).
2. **RegistrationAgent**
- Wraps `startRegistration()`, `startUnregistration()`.
- Handles callback URL → `handleWearablesCallback(url:)`.
- Exposes a simplified `RegistrationState` to rest of the app.
3. **DeviceAgent**
- Wraps `devicesStream()`.
- Maintains list of `Device` objects and selection logic.
- Creates `AutoDeviceSelector` and `SpecificDeviceSelector` as needed.
4. **PermissionsAgent**
- Wraps camera permission APIs.
- Exposes computed permission state and helper methods:
- `ensureCameraPermission()` → `Bool`
- `requireCameraPermission { ... }`
5. **SessionAgent**
- Owns `StreamSession` instances.
- Manages `StreamSessionConfig`.
- Subscribes to `statePublisher` and `errorPublisher`.
- Provides high-level states: `.idle`, `.starting`, `.running`, `.paused`, `.stopping`, `.error`.
6. **CameraAgent**
- Subscribes to `videoFramePublisher` and `photoDataPublisher`.
- Handles conversion to `UIImage` or CV/ML inputs.
- Encapsulates capture flows (trigger, save, share).
7. **AudioAgent**
- Configures `AVAudioSession`.
- Manages Bluetooth routing, route changes.
- Provides high-level methods for starting/stopping audio use.
8. **UIAgent**
- Connects agents to SwiftUI / UIKit views.
- Owns view models modeling the whole lifecycle:
- Not registered → needs registration
- Registered but no permission
- Permission granted but no device
- Device ready, session not started
- Session active / paused / stopped / error
9. **MockAgent**
- Uses Mock Device Kit when available.
- Simulates devices, streams, errors.
- Enables automated tests and offline dev.
### 8.2 Coding guidelines
- Keep direct SDK calls inside agents; UI interacts with our abstractions only.
- Prefer async/await and structured concurrency over nested callbacks.
- Treat publishers / async streams as authoritative sources of truth.
- Always handle `StreamSessionError` & permission failures gracefully.
- Log key events (state changes, errors, route changes) for telemetry & debugging.
### 8.3 Mock Device Implementation (Debug)
For development without physical devices, `MWDATMockDevice` allows simulating connection, state, and media.
**Setup in Debug Build:**
```swift
#if DEBUG
import MWDATMockDevice
class MockManager {
let mockDeviceKit = MockDeviceKit.shared
var mockDevice: MockDevice?
func pairMockDevice() {
// Creates and pairs a simulated Ray-Ban Meta device
let device = mockDeviceKit.pairRaybanMeta()
self.mockDevice = device
// Simulate physical state to allow streaming
device.powerOn()
device.unfold()
device.don() // "Put on" the glasses
}
func setupMockMedia(videoURL: URL, imageURL: URL) async {
guard let cameraKit = (mockDevice as? MockDisplaylessGlasses)?.getCameraKit() else { return }
// Feed a local video file as the camera stream
await cameraKit.setCameraFeed(fileURL: videoURL)
// Set the image to be returned when capturePhoto() is called
await cameraKit.setCapturedImage(fileURL: imageURL)
}
}
#endif
```
**Mock Controls:**
- `powerOn() / powerOff()`
- `fold() / unfold()`: Closing hinges stops the stream.
- `don() / doff()`: Wearing state affects auto-pause behavior.
### 8.4 Common UI Patterns
**Video Rendering in SwiftUI:**
```swift
struct StreamView: View {
let videoFrame: UIImage
var body: some View {
GeometryReader { geometry in
Image(uiImage: videoFrame)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: geometry.size.width, height: geometry.size.height)
.clipped()
}
.edgesIgnoringSafeArea(.all)
}
}
```
**Loading State:**
Use a boolean flag `hasReceivedFirstFrame` (set to true on the first video frame emission) to toggle between a loading spinner and the video view. This prevents showing a black screen while the connection initializes.
---
## 9. Edge cases & pitfalls
- **Permission denial loop**: if user keeps denying camera access, avoid spamming prompts; instead surface instructions to enable permissions in Meta AI.
- **Multiple devices**: don’t assume only one; ensure selectors and UI handle multiple glasses.
- **Session conflicts**: only one session per device; handle failures when another app already has a session.
- **Bluetooth instability**: expect transient disconnects; implement reconnection / recovery strategies.
- **Capture without streaming**: `capturePhoto` returns `false` if no active session or capture in progress.
- **Mutating `sampleBuffer`**: never mutate `VideoFrame.sampleBuffer`.
- **Quality expectations**: communicate potential quality degradation due to bandwidth to stakeholders/UX.
---
## 10. Maintenance rules for AGENTS.MD
Whenever we:
- Upgrade the DAT SDK version.
- Add new capabilities (codecs, resolutions, devices).
- Change our architecture around sessions, permissions, audio.
We must:
1. Update all relevant API signatures & enums **here**.
2. Add sections for any new major features.
3. Mark removed/changed APIs as deprecated and document migration paths.
This keeps AI agents and human developers aligned on the current integration contract.