This document describes the implementation of Push-to-Talk (PTT) support for AirPods and other standard Bluetooth earbuds in the Resgrid Unit app.
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.
-
MediaButtonService (
src/services/media-button.service.ts)- Singleton service that manages media button event listeners
- Handles double-tap detection
- Provides PTT toggle/push-to-talk modes
- Integrates with LiveKit for microphone control
-
Native Modules
- iOS:
MediaButtonModule.swift- UsesMPRemoteCommandCenterto capture media control events - Android:
MediaButtonModule.kt- UsesMediaSessionto capture media button events
- iOS:
-
Store Updates (
src/stores/app/bluetooth-audio-store.ts)- Added
MediaButtonPTTSettingsinterface - Added settings management actions
- Added
-
LiveKit Integration (
src/stores/app/livekit-store.ts)- Initializes media button service when connecting to a room
- Cleans up service when disconnecting
- When a LiveKit room is connected, the
MediaButtonModulesets upMPRemoteCommandCenterlisteners - Play/Pause button presses on AirPods trigger the
togglePlayPauseCommand - The event is sent to JavaScript via
NativeEventEmitter MediaButtonServiceprocesses the event and toggles the microphone state
- When a LiveKit room is connected, the
MediaButtonModulecreates aMediaSession - Button presses are captured via the
MediaSession.Callback - The event is sent to JavaScript via
DeviceEventManagerModule MediaButtonServiceprocesses the event and toggles the microphone state
- Single press toggles between muted and unmuted states
- Best for hands-free operation
- Press and hold to unmute
- Release to mute
- Better for traditional radio-style communication
The MediaButtonPTTSettings interface provides the following configuration:
interface MediaButtonPTTSettings {
enabled: boolean; // Enable/disable media button PTT
pttMode: 'toggle' | 'push_to_talk';
usePlayPauseForPTT: boolean; // Use play/pause button for PTT
doubleTapAction: 'none' | 'toggle_mute';
doubleTapTimeoutMs: number; // Default: 400ms
}import { useBluetoothAudioStore } from '@/stores/app/bluetooth-audio-store';
// Enable media button PTT
useBluetoothAudioStore.getState().setMediaButtonPTTEnabled(true);
// Update settings
useBluetoothAudioStore.getState().setMediaButtonPTTSettings({
pttMode: 'push_to_talk',
doubleTapAction: 'toggle_mute',
});import { mediaButtonService } from '@/services/media-button.service';
// Enable microphone
await mediaButtonService.enableMicrophone();
// Disable microphone
await mediaButtonService.disableMicrophone();
// Update settings
mediaButtonService.updateSettings({
pttMode: 'toggle',
});The service provides audio feedback for PTT actions:
playStartTransmittingSound()- Played when microphone is enabledplayStopTransmittingSound()- Played when microphone is disabled
- Apple AirPods (all generations)
- Apple AirPods Pro
- Apple AirPods Max
- Samsung Galaxy Buds
- Sony WF/WH series
- Jabra Elite series
- Any Bluetooth earbuds with media control buttons
- Background Mode: iOS requires CallKeep to be active for background audio support
- Button Mapping: Some earbuds may have non-standard button mappings
- Double-Tap Detection: Natural double-tap gestures on AirPods may conflict with the double-tap PTT action
- Ensure Bluetooth is connected and the earbuds are the active audio device
- Check that
mediaButtonPTTSettings.enabledistrue - On iOS, ensure the app has audio session properly configured
- On Android, check that no other app is capturing media button events
- Adjust
doubleTapTimeoutMsto a lower value if not using double-tap feature - Set
doubleTapActionto'none'for immediate response
src/services/media-button.service.ts- Main TypeScript servicesrc/services/__tests__/media-button.service.test.ts- Testsplugins/withMediaButtonModule.js- Expo config plugin (generates native modules during prebuild)docs/airpods-ptt-support.md- This documentation
src/stores/app/bluetooth-audio-store.ts- Added media button settingssrc/stores/app/livekit-store.ts- Integration with room connection/disconnectionapp.config.ts- Added config plugin reference
The following native files are generated automatically by withMediaButtonModule.js during expo prebuild:
iOS:
ios/ResgridUnit/MediaButtonModule.swift- iOS native module using MPRemoteCommandCenterios/ResgridUnit/MediaButtonModule.m- Objective-C bridge file- Updates
ResgridUnit-Bridging-Header.hwith required React Native imports
Android:
android/app/src/main/java/{package}/MediaButtonModule.kt- Android native module using MediaSessionandroid/app/src/main/java/{package}/MediaButtonPackage.kt- React Native package registration- Updates
MainApplication.ktto register the MediaButtonPackage
Since this project uses Expo with prebuild, the native modules are generated automatically:
# Clean and regenerate native projects
npx expo prebuild --clean
# Or for specific platform
npx expo prebuild --platform ios --clean
npx expo prebuild --platform android --clean
# Then build normally
yarn ios # or yarn android