Skip to content

Commit 5c7c8de

Browse files
authored
Merge pull request #199 from Resgrid/develop
Develop
2 parents 2bd8ea1 + 375900b commit 5c7c8de

File tree

15 files changed

+1654
-29
lines changed

15 files changed

+1654
-29
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Best Practices:
4848
- Handle errors gracefully and provide user feedback.
4949
- Implement proper offline support.
5050
- Ensure the user interface is intuitive and user-friendly and works seamlessly across different devices and screen sizes.
51+
- This is an expo managed project that uses prebuild, do not make native code changes outside of expo prebuild capabilities.
5152

5253
Additional Rules:
5354

app.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
207207
'./customManifest.plugin.js',
208208
'./plugins/withForegroundNotifications.js',
209209
'./plugins/withNotificationSounds.js',
210+
'./plugins/withMediaButtonModule.js',
210211
['app-icon-badge', appIconBadgeConfig],
211212
],
212213
extra: {

docs/airpods-ptt-support.md

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# AirPods/Bluetooth Earbuds PTT Support
2+
3+
This document describes the implementation of Push-to-Talk (PTT) support for AirPods and other standard Bluetooth earbuds in the Resgrid Unit app.
4+
5+
## Overview
6+
7+
The implementation adds support for using media button presses from AirPods, Galaxy Buds, and other Bluetooth earbuds to control the microphone mute/unmute state during LiveKit voice calls.
8+
9+
## Architecture
10+
11+
### Components
12+
13+
1. **MediaButtonService** (`src/services/media-button.service.ts`)
14+
- Singleton service that manages media button event listeners
15+
- Handles double-tap detection
16+
- Provides PTT toggle/push-to-talk modes
17+
- Integrates with LiveKit for microphone control
18+
19+
2. **Native Modules**
20+
- **iOS**: `MediaButtonModule.swift` - Uses `MPRemoteCommandCenter` to capture media control events
21+
- **Android**: `MediaButtonModule.kt` - Uses `MediaSession` to capture media button events
22+
23+
3. **Store Updates** (`src/stores/app/bluetooth-audio-store.ts`)
24+
- Added `MediaButtonPTTSettings` interface
25+
- Added settings management actions
26+
27+
4. **LiveKit Integration** (`src/stores/app/livekit-store.ts`)
28+
- Initializes media button service when connecting to a room
29+
- Cleans up service when disconnecting
30+
31+
## How It Works
32+
33+
### iOS (AirPods)
34+
35+
1. When a LiveKit room is connected, the `MediaButtonModule` sets up `MPRemoteCommandCenter` listeners
36+
2. Play/Pause button presses on AirPods trigger the `togglePlayPauseCommand`
37+
3. The event is sent to JavaScript via `NativeEventEmitter`
38+
4. `MediaButtonService` processes the event and toggles the microphone state
39+
40+
### Android (Bluetooth Earbuds)
41+
42+
1. When a LiveKit room is connected, the `MediaButtonModule` creates a `MediaSession`
43+
2. Button presses are captured via the `MediaSession.Callback`
44+
3. The event is sent to JavaScript via `DeviceEventManagerModule`
45+
4. `MediaButtonService` processes the event and toggles the microphone state
46+
47+
## PTT Modes
48+
49+
### Toggle Mode (Default)
50+
- Single press toggles between muted and unmuted states
51+
- Best for hands-free operation
52+
53+
### Push-to-Talk Mode
54+
- Press and hold to unmute
55+
- Release to mute
56+
- Better for traditional radio-style communication
57+
58+
## Settings
59+
60+
The `MediaButtonPTTSettings` interface provides the following configuration:
61+
62+
```typescript
63+
interface MediaButtonPTTSettings {
64+
enabled: boolean; // Enable/disable media button PTT
65+
pttMode: 'toggle' | 'push_to_talk';
66+
usePlayPauseForPTT: boolean; // Use play/pause button for PTT
67+
doubleTapAction: 'none' | 'toggle_mute';
68+
doubleTapTimeoutMs: number; // Default: 400ms
69+
}
70+
```
71+
72+
## Usage
73+
74+
### Enabling/Disabling
75+
```typescript
76+
import { useBluetoothAudioStore } from '@/stores/app/bluetooth-audio-store';
77+
78+
// Enable media button PTT
79+
useBluetoothAudioStore.getState().setMediaButtonPTTEnabled(true);
80+
81+
// Update settings
82+
useBluetoothAudioStore.getState().setMediaButtonPTTSettings({
83+
pttMode: 'push_to_talk',
84+
doubleTapAction: 'toggle_mute',
85+
});
86+
```
87+
88+
### Manual Control (Advanced)
89+
```typescript
90+
import { mediaButtonService } from '@/services/media-button.service';
91+
92+
// Enable microphone
93+
await mediaButtonService.enableMicrophone();
94+
95+
// Disable microphone
96+
await mediaButtonService.disableMicrophone();
97+
98+
// Update settings
99+
mediaButtonService.updateSettings({
100+
pttMode: 'toggle',
101+
});
102+
```
103+
104+
## Audio Feedback
105+
106+
The service provides audio feedback for PTT actions:
107+
- `playStartTransmittingSound()` - Played when microphone is enabled
108+
- `playStopTransmittingSound()` - Played when microphone is disabled
109+
110+
## Supported Devices
111+
112+
### Tested
113+
- Apple AirPods (all generations)
114+
- Apple AirPods Pro
115+
- Apple AirPods Max
116+
117+
### Expected to Work
118+
- Samsung Galaxy Buds
119+
- Sony WF/WH series
120+
- Jabra Elite series
121+
- Any Bluetooth earbuds with media control buttons
122+
123+
## Limitations
124+
125+
1. **Background Mode**: iOS requires CallKeep to be active for background audio support
126+
2. **Button Mapping**: Some earbuds may have non-standard button mappings
127+
3. **Double-Tap Detection**: Natural double-tap gestures on AirPods may conflict with the double-tap PTT action
128+
129+
## Troubleshooting
130+
131+
### Media buttons not working
132+
133+
1. Ensure Bluetooth is connected and the earbuds are the active audio device
134+
2. Check that `mediaButtonPTTSettings.enabled` is `true`
135+
3. On iOS, ensure the app has audio session properly configured
136+
4. On Android, check that no other app is capturing media button events
137+
138+
### Delays in response
139+
140+
- Adjust `doubleTapTimeoutMs` to a lower value if not using double-tap feature
141+
- Set `doubleTapAction` to `'none'` for immediate response
142+
143+
## Files Modified/Created
144+
145+
### New Files
146+
- `src/services/media-button.service.ts` - Main TypeScript service
147+
- `src/services/__tests__/media-button.service.test.ts` - Tests
148+
- `plugins/withMediaButtonModule.js` - Expo config plugin (generates native modules during prebuild)
149+
- `docs/airpods-ptt-support.md` - This documentation
150+
151+
### Modified Files
152+
- `src/stores/app/bluetooth-audio-store.ts` - Added media button settings
153+
- `src/stores/app/livekit-store.ts` - Integration with room connection/disconnection
154+
- `app.config.ts` - Added config plugin reference
155+
156+
### Generated During Prebuild (via config plugin)
157+
The following native files are generated automatically by `withMediaButtonModule.js` during `expo prebuild`:
158+
159+
**iOS:**
160+
- `ios/ResgridUnit/MediaButtonModule.swift` - iOS native module using MPRemoteCommandCenter
161+
- `ios/ResgridUnit/MediaButtonModule.m` - Objective-C bridge file
162+
- Updates `ResgridUnit-Bridging-Header.h` with required React Native imports
163+
164+
**Android:**
165+
- `android/app/src/main/java/{package}/MediaButtonModule.kt` - Android native module using MediaSession
166+
- `android/app/src/main/java/{package}/MediaButtonPackage.kt` - React Native package registration
167+
- Updates `MainApplication.kt` to register the MediaButtonPackage
168+
169+
## Build Instructions
170+
171+
Since this project uses Expo with prebuild, the native modules are generated automatically:
172+
173+
```bash
174+
# Clean and regenerate native projects
175+
npx expo prebuild --clean
176+
177+
# Or for specific platform
178+
npx expo prebuild --platform ios --clean
179+
npx expo prebuild --platform android --clean
180+
181+
# Then build normally
182+
yarn ios # or yarn android
183+
```

env.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ const client = z.object({
8888
LOGGING_KEY: z.string(),
8989
APP_KEY: z.string(),
9090
UNIT_MAPBOX_PUBKEY: z.string(),
91-
UNIT_MAPBOX_DLKEY: z.string(),
9291
IS_MOBILE_APP: z.boolean(),
9392
SENTRY_DSN: z.string(),
9493
COUNTLY_APP_KEY: z.string(),
@@ -123,7 +122,6 @@ const _clientEnv = {
123122
APP_KEY: process.env.UNIT_APP_KEY || '',
124123
IS_MOBILE_APP: true, // or whatever default you want
125124
UNIT_MAPBOX_PUBKEY: process.env.UNIT_MAPBOX_PUBKEY || '',
126-
UNIT_MAPBOX_DLKEY: process.env.UNIT_MAPBOX_DLKEY || '',
127125
SENTRY_DSN: process.env.UNIT_SENTRY_DSN || '',
128126
COUNTLY_APP_KEY: process.env.UNIT_COUNTLY_APP_KEY || '',
129127
COUNTLY_SERVER_URL: process.env.UNIT_COUNTLY_SERVER_URL || '',

0 commit comments

Comments
 (0)