Skip to content

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.ktstartAudioCapture() at line 624, startStreamSession() at line 434 - main/app/wearables-module/android/src/main/java/expo/modules/wearablesmodule/WearablesHelpers.ktcreateStreamSession() 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

  1. Pair Meta Ray-Ban glasses as Bluetooth audio device on Android.
  2. Set selectedDevice to "glasses".
  3. Start capture.
  4. Observe capture_audio_started in logs (audio starts successfully).
  5. Observe glasses LED does NOT turn on.
  6. 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/A with reason)
  • [x] Verified no duplicate solved-bug log exists for same root cause