react-native-voice-hold
v1.0.7
Published
React Native Voice library with enhanced hold recording functionality and React Native 0.80+ compatibility fixes
Maintainers
Readme
🎙️ react-native-voice-hold
Enhanced React Native Voice Recognition Library with Hold Recording Support
A robust React Native voice recognition library with critical bug fixes for React Native 0.76+ and the New Architecture. This package resolves the major event listener issues that break speech-to-text functionality in the latest React Native versions.
🚨 Key Fixes & Improvements
✅ React Native 0.76+ Compatibility
- Fixed: Critical event listener bug in React Native 0.76+ with New Architecture
- Solution: Implemented
DeviceEventEmitterpattern for reliable event handling - Result: Speech-to-text results now work properly in RN 0.76+ and 0.80+
🔥 Critical Fix: Empty Final Results
- Issue: Android Speech Recognition API sometimes returns empty final results even when partial results contain valid transcription
- Symptoms:
onSpeechResultscallback receives empty array[]despite successful partial results - Root Cause: Device-specific speech recognition quirks, short speech segments, background noise, timing issues
- Solution: Implemented comprehensive fallback mechanism using partial results
- Timeout Fallback: 1-second timeout ensures transcript processing even if final results fail
- Partial Result Storage: Stores meaningful partial results for fallback use
- Smart Fallback Logic: Uses partial results when final results are empty
- Error Recovery: Proper cleanup and interruption handling
🎯 Enhanced Hold Recording
- New:
startHoldRecording()andstopHoldRecording()methods - Feature: Continuous recording without auto-stop timeouts
- Use Case: Perfect for voice messages and long-form dictation
🔧 Production Ready
- Architecture: Full New Architecture (TurboModules) support
- Platforms: iOS 11+ and Android API 21+
- Stability: Comprehensive error handling and state management
📱 Installation
npm install react-native-voice-holdiOS Setup (Required)
cd ios && pod installAndroid Setup
Add microphone permissions to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />🚀 Quick Start
Basic Usage (Fixed for RN 0.76+)
import Voice from 'react-native-voice-hold';
import { DeviceEventEmitter } from 'react-native';
const VoiceComponent = () => {
const [results, setResults] = useState<string[]>([]);
const [isListening, setIsListening] = useState(false);
useEffect(() => {
// ✅ CRITICAL: Use DeviceEventEmitter for RN 0.76+ compatibility
const onSpeechResults = DeviceEventEmitter.addListener(
'onSpeechResults',
(e: any) => {
console.log('📝 Speech results:', e.value);
setResults(e.value);
}
);
const onSpeechStart = DeviceEventEmitter.addListener(
'onSpeechStart',
() => {
console.log('🎤 Speech started');
setIsListening(true);
}
);
const onSpeechEnd = DeviceEventEmitter.addListener(
'onSpeechEnd',
() => {
console.log('🔇 Speech ended');
setIsListening(false);
}
);
return () => {
onSpeechResults.remove();
onSpeechStart.remove();
onSpeechEnd.remove();
};
}, []);
const startListening = async () => {
try {
await Voice.start('en-US');
} catch (error) {
console.error('❌ Error starting voice recognition:', error);
}
};
const stopListening = async () => {
try {
await Voice.stop();
} catch (error) {
console.error('❌ Error stopping voice recognition:', error);
}
};
return (
<View>
<Button
title={isListening ? 'Stop Listening' : 'Start Listening'}
onPress={isListening ? stopListening : startListening}
/>
{results.map((result, index) => (
<Text key={index}>{result}</Text>
))}
</View>
);
};Hold Recording (New Feature)
import Voice from 'react-native-voice-hold';
// Start continuous recording (perfect for voice messages)
const startHoldRecording = async () => {
try {
await Voice.startHoldRecording('en-US', {
// Hold mode disables silence timeouts
continuous: true,
maximumWaitTime: 300000, // 5 minutes max
});
console.log('🎙️ Hold recording started');
} catch (error) {
console.error('❌ Error starting hold recording:', error);
}
};
// Stop hold recording manually
const stopHoldRecording = async () => {
try {
await Voice.stopHoldRecording();
console.log('⏹️ Hold recording stopped');
} catch (error) {
console.error('❌ Error stopping hold recording:', error);
}
};🔥 Critical Fix: Empty Final Results Solution
Hook Implementation (Recommended)
import { useVoiceRecognition } from 'react-native-voice-hold';
const VoiceAssistant = () => {
const [state, actions] = useVoiceRecognition({
locale: 'en-US',
onStart: () => {
console.log('🎤 Speech started');
// Clear any existing timeout and reset state
},
onEnd: () => {
console.log('🔇 Speech ended - processing...');
// 🔥 CRITICAL FIX: 1-second timeout ensures transcript processing
// even if final results don't come
},
onResults: results => {
console.log('📝 Final results received:', results);
if (results.length > 0) {
// Process final results normally
processTranscript(results[0]);
} else {
// 🔥 CRITICAL FIX: Use partial results as fallback
console.log('⚠️ Final results empty, using partial results fallback');
// Hook automatically handles fallback logic
}
},
onPartialResults: partial => {
console.log('🔄 Partial results:', partial);
// 🔥 CRITICAL FIX: Hook stores partial results for fallback
},
});
// The hook automatically handles all fallback logic!
return (
<View>
<TouchableOpacity
onPressIn={() => actions.startHoldRecording()}
onPressOut={() => actions.stopListening()}
>
<Text>Hold to Speak</Text>
</TouchableOpacity>
</View>
);
};Direct Library Implementation
import React, { useEffect, useState, useRef } from 'react';
import { DeviceEventEmitter, Platform } from 'react-native';
import Voice from 'react-native-voice-hold';
const VoiceComponent = () => {
const [results, setResults] = useState<string[]>([]);
const [isListening, setIsListening] = useState(false);
// 🔥 CRITICAL FIX: Add refs for fallback mechanism
const lastPartialResult = useRef<string>('');
const resultsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isInterrupted = useRef<boolean>(false);
useEffect(() => {
const onSpeechStart = DeviceEventEmitter.addListener('onSpeechStart', () => {
console.log('🎤 Speech started');
setIsListening(true);
// Clear any existing timeout
if (resultsTimeoutRef.current) {
clearTimeout(resultsTimeoutRef.current);
resultsTimeoutRef.current = null;
}
lastPartialResult.current = '';
});
const onSpeechEnd = DeviceEventEmitter.addListener('onSpeechEnd', () => {
console.log('🔇 Speech ended - processing...');
// 🔥 CRITICAL FIX: Add timeout to ensure we process transcript
resultsTimeoutRef.current = setTimeout(() => {
console.log('⏰ Timeout: No final results received, using partial results');
const fallbackTranscript = lastPartialResult.current;
if (fallbackTranscript.trim() && !isInterrupted.current) {
console.log('🔄 Timeout fallback transcript:', fallbackTranscript);
handleTranscript(fallbackTranscript);
}
}, 1000); // Wait 1 second for final results
});
const onSpeechResults = DeviceEventEmitter.addListener('onSpeechResults', (e) => {
console.log('📝 Speech results:', e.value);
// Clear the timeout since we got results
if (resultsTimeoutRef.current) {
clearTimeout(resultsTimeoutRef.current);
resultsTimeoutRef.current = null;
}
if (e.value && e.value.length > 0) {
const finalTranscript = e.value[0];
console.log('⚡ Fast transcript:', finalTranscript);
handleTranscript(finalTranscript);
} else {
// 🔥 CRITICAL FIX: Use partial results as fallback
console.log('⚠️ Final results empty, checking partial results...');
const fallbackTranscript = lastPartialResult.current;
if (fallbackTranscript.trim()) {
console.log('🔄 Using fallback transcript:', fallbackTranscript);
handleTranscript(fallbackTranscript);
}
}
});
const onSpeechPartialResults = DeviceEventEmitter.addListener('onSpeechPartialResults', (e) => {
console.log('🔄 Partial results:', e.value);
if (e.value && e.value.length > 0) {
const partialTranscript = e.value[0];
// 🔥 CRITICAL FIX: Store the last partial result for fallback
if (partialTranscript.trim()) {
lastPartialResult.current = partialTranscript;
}
}
});
return () => {
onSpeechStart.remove();
onSpeechEnd.remove();
onSpeechResults.remove();
onSpeechPartialResults.remove();
// Clear timeout on cleanup
if (resultsTimeoutRef.current) {
clearTimeout(resultsTimeoutRef.current);
resultsTimeoutRef.current = null;
}
Voice.destroy();
};
}, []);
const handleTranscript = (transcript: string) => {
console.log('🎯 Processing transcript:', transcript);
setResults([transcript]);
};
// ... rest of component
};📖 Complete API Reference
Standard Methods
Voice.start(locale)- Start voice recognitionVoice.stop()- Stop voice recognitionVoice.cancel()- Cancel voice recognitionVoice.destroy()- Clean up resourcesVoice.isAvailable()- Check if voice recognition is availableVoice.isRecognizing()- Check if currently recognizing
Hold Recording Methods (New)
Voice.startHoldRecording(locale, options)- Start continuous recordingVoice.stopHoldRecording()- Stop hold recording
Event Listeners (DeviceEventEmitter - RN 0.76+ Compatible)
// ✅ CORRECT: Use DeviceEventEmitter
DeviceEventEmitter.addListener('onSpeechStart', handler);
DeviceEventEmitter.addListener('onSpeechRecognized', handler);
DeviceEventEmitter.addListener('onSpeechEnd', handler);
DeviceEventEmitter.addListener('onSpeechError', handler);
DeviceEventEmitter.addListener('onSpeechResults', handler);
DeviceEventEmitter.addListener('onSpeechPartialResults', handler);
DeviceEventEmitter.addListener('onSpeechVolumeChanged', handler);
// ❌ BROKEN: Direct assignment (doesn't work in RN 0.76+)
Voice.onSpeechResults = handler; // DON'T USE🔄 Migration from react-native-voice
Install the new package:
npm uninstall @react-native-voice/voice npm install react-native-voice-holdUpdate your imports:
// Old
import Voice from '@react-native-voice/voice';
// New import Voice from 'react-native-voice-hold';
3. **Update event listeners for RN 0.76+ compatibility:**
```typescript
// Old (broken in RN 0.76+)
Voice.onSpeechResults = (e) => console.log(e.value);
// New (works in all RN versions)
DeviceEventEmitter.addListener('onSpeechResults', (e) => console.log(e.value));- Add fallback logic for empty final results:
// The useVoiceRecognition hook automatically handles this! // Or implement the fallback pattern shown above for direct usage
🐛 Troubleshooting
Empty Final Results
Problem: onSpeechResults receives empty array despite successful partial results.
Solution: ✅ Fixed in this package - The hook and examples above include comprehensive fallback logic.
Event Listeners Not Working (RN 0.76+)
Problem: Speech events not firing in React Native 0.76+.
Solution: ✅ Fixed - Use DeviceEventEmitter pattern instead of direct assignment.
Hold Recording Not Working
Problem: Hold recording stops unexpectedly.
Solution: ✅ Fixed - Proper hold mode state management prevents premature stopping.
📄 License
MIT License - see LICENSE file for details.
🤝 Contributing
Contributions are welcome! Please read our contributing guidelines and submit pull requests.
Built with ❤️ for the React Native community
