Glasses LED Does Not Activate When Recording on Android
Metadata
- Date:
2026-04-18 - Status:
fixed - Severity:
high - Related issue/ticket:
N/A - Owner:
N/A
About
Overview: - On Android with selectedDevice="glasses", logs show capture_audio_started and recording proceeds, but the glasses LED light never turns on. The LED is the user-visible indicator that the glasses are in recording mode. - This bug is important because users have no feedback that the glasses are active, and it indicates the Wearables SDK is never told to enter capture mode.
Technical Questions: - startAudioCapture() on Android routes Bluetooth HFP audio via raw Android AudioRecord + VOICE_COMMUNICATION source. This captures audio FROM the glasses but never signals the glasses firmware to enter recording mode. - The glasses LED is controlled by the Meta Wearables DAT SDK's StreamSession (activated via Wearables.startStreamSession()). Starting a StreamSession is what tells the glasses "you are being recorded" and triggers the LED. - On iOS with selectedDevice="glasses", startStreamSession() IS called after startAudioCapture(). On Android, the condition if (Platform.OS !== "android" && activeRecordingDevice !== "glasses-audio") unconditionally skips it. - Bug is confined to Android + glasses mode. glasses-audio mode on either platform also skips startStreamSession(), meaning the LED would also not activate there.
Resources: - main/app/lib/capture-session.ts — line 211: condition that skips startStreamSession() on Android - main/app/wearables-module/android/src/main/java/expo/modules/wearablesmodule/WearablesModule.kt — startAudioCapture() at line 624, startStreamSession() at line 434 - main/app/wearables-module/android/src/main/java/expo/modules/wearablesmodule/WearablesHelpers.kt — createStreamSession() at line 31 - main/app/__tests__/capture-session.test.ts — existing tests assert startStreamSession is NOT called on Android
Steps to cause failure
flowchart LR
StartCapture --> SelectedDevice{"selectedDevice=glasses\nplatform=android"}
SelectedDevice --> StartAudio["startAudioCapture()"]
StartAudio --> HFP["AudioRecord via Bluetooth HFP\n(captures audio)"]
HFP --> LogStarted["capture_audio_started logged"]
LogStarted --> NoSDKSignal["startStreamSession() NEVER CALLED"]
NoSDKSignal --> LEDOff["Glasses LED stays off\n(SDK never told to record)"] System
flowchart TD
CaptureSession --> WearablesModuleTS["WearablesModule (TS)"]
WearablesModuleTS --> WearablesModuleKt["WearablesModule.kt (Android)"]
WearablesModuleKt --> AudioRecord["Android AudioRecord\n(HFP audio capture)"]
WearablesModuleKt --> DatSDK["Meta Wearables DAT SDK\nWearables.startStreamSession()"]
DatSDK --> GlassesFirmware["Glasses Firmware\n(controls LED)"]
AudioRecord --> GlassesBluetooth["Glasses BT Mic\n(no LED feedback)"] On Android, startAudioCapture() uses only the AudioRecord path. The DAT SDK path is never invoked. The LED is gated by the DAT SDK's stream session state.
Reproduction Details
- Pair Meta Ray-Ban glasses as Bluetooth audio device on Android.
- Set
selectedDeviceto"glasses". - Start capture.
- Observe
capture_audio_startedin logs (audio starts successfully). - Observe glasses LED does NOT turn on.
- Expected: glasses LED activates when
startStreamSession()is called on the DAT SDK.
Reproduction test: cd main/app && npx jest --testPathPattern=capture-session.test.ts --runInBand
Notes for PR
Root cause: - capture-session.ts line 211 gates startStreamSession() behind Platform.OS !== "android". On Android, only startAudioCapture() (raw HFP) is called. The glasses DAT SDK is never given the signal to enter recording mode, so the LED stays off.
Fix summary: - Split the condition at line 211 into two guards: 1. if (activeRecordingDevice !== "glasses-audio") — call startStreamSession() on both iOS and Android to activate the glasses recording mode / LED. 2. if (Platform.OS !== "android") (nested) — only call startRecordingFrames() on iOS (frame recording is iOS-only). - Mirrored the same split in stopCapture() and cleanupSession(): call stopStreamSession() when activeRecordingDevice !== "glasses-audio" (not gated by platform). - Also fixed a pre-existing jest hoisting bug in capture-session.test.ts introduced in commit 719be108: const mockLogger = jest.fn() was in TDZ when the hoisted jest.mock factory ran. Changed to use jest.fn(() => jest.fn()) inside the factory and access the mock via createFlowLogger.mock.results[0].value.
Audit Log
| ID | Action | Note | Context |
|---|---|---|---|
| 1 | Create audit log | Initialize bug investigation | issue created |
| 2 | Trace logs to source | capture_audio_started confirms HFP audio started; no subsequent SDK call found | root-cause analysis |
| 3 | Read capture-session.ts | Confirmed condition at line 211 skips startStreamSession() on Android | source trace |
| 4 | Read WearablesModule.kt | Confirmed startStreamSession() calls Wearables.startStreamSession() which creates a StreamSession (DAT SDK call that signals glasses firmware) | SDK boundary trace |
| 5 | Read capture-session.test.ts | Existing tests at lines 123–128 explicitly assert startStreamSession is NOT called on Android — these tests encode the broken behavior and must be updated | test audit |
| 6 | Fix pre-existing test infra bug | const mockLogger = jest.fn() was TDZ when hoisted jest.mock factory ran; all 16 tests were failing. Switched to jest.fn(() => jest.fn()) inside factory with createFlowLogger.mock.results[0].value accessor | test fix |
| 7 | Add failing reproduction tests | Added android starts audio-only capture with stream session (for LED) but no frame recording and android stop calls stopStreamSession to deactivate glasses LED; confirmed they fail before fix | reproduction |
| 8 | Fix capture-session.ts | Split Platform.OS !== "android" condition: startStreamSession() now called on both platforms for glasses mode; startRecordingFrames() still iOS-only. Applied same split to stopCapture() and cleanupSession() | source fix |
| 9 | Verify all tests pass | 16/16 tests pass after fix | validation |
Verification
- [x] Reproduced failure before fix
- [x] Reproduction test fails before fix
- [x] Root cause identified with evidence
- [x] Fix applied at source (no workaround-only patch)
- [x] Reproduction test passes after fix
- [x] Reproduction path now passes
- [x] Regression test added/updated (or
N/Awith reason) - [x] Verified no duplicate solved-bug log exists for same root cause