React Native app intermittently freezes while recording and sending 5-second audio chunks over a socket
!(https://i.sstatic.net/pzsQw8Xf.png)
I am implementing audio recording in a React Native app. I record audio in 5-second WAV chunks and send each chunk to my backend through a socket.
The app sometimes freezes during recording or sometime crash. The issue is intermittent and does not happen with every recording.
My recording flow is:
Initialize the recorder and start recording.
After 5 seconds (CHUNK_DURATION = 5), stop recording and get the saved file path.
Start recording the next chunk immediately.
Read the previous chunk into an ArrayBuffer, convert it to a Uint8Array, and pass it to handleUploadVoice, which handles sending it through the socket.
I also update a countdown every second and start a background service on Android.
Expected behavior: Recording and socket uploads should continue while the UI remains responsive.
Actual behavior: The app occasionally becomes unresponsive during recording.
Here is a screenshot of the recording screen:
Recording and chunk-processing code:
const onStartRecord = async () => {
session_Id = generateUID();
const permissionGranted = await requestAudioPermission();
if (!permissionGranted) {
Alert.alert('Permission Denied', 'Microphone permission is required.');
return;
}
setIsRecording(true);
const toastMsg =
'Recording started. Please speak clearly into the microphone. Avoid background noise. Speak close to the mic for best results.';
Toast.show(toastMsg);
if (Platform.OS === 'android') {
try {
const options = {
taskName: 'Recording',
taskTitle: 'Recording Audio',
taskDesc: 'Your voice note is being recorded in the background.',
taskIcon: {
name: 'ic_launcher',
type: 'mipmap',
},
foregroundServiceType: ['microphone'], // Added for Android 14+ compatibility
parameters: {
delay: 1000,
},
};
await BackgroundService.start(backgroundTask, options);
} catch (e) {
console.log('Failed to start foreground service', e);
}
}
setIsRecorded(false);
setIsWaveAnimating(true);
setAudioArray([]);
setCountdown(3600);
setTotalDuration(0);
chunkCounter.current = 0; // Reset counter
recordingSessionActive.current = true;
isProcessingChunk.current = false;
masterTimerRef.current = setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
clearInterval(masterTimerRef.current);
onStopRecord();
return 0;
}
if (prev === 61) {
// 61 because next tick will make it 60
setShowReminder(true);
// setTimeout(() => setShowReminder(false), 5000); // hide after 5s if you want
}
return prev - 1;
});
}, 1000);
// Call startNextChunk only ONCE to begin the process.
startNextChunk();
};
const startNextChunk = async () => {
if (!recordingSessionActive.current) {
return;
}
try {
chunkCounter.current += 1;
const currentChunkNumber = chunkCounter.current;
const fileName = `chunk_${session_Id}_${currentChunkNumber}.wav`;
const options = {
sampleRate: 16000,
channels: 1,
bitsPerSample: 16,
audioSource: 6,
wavFile: fileName,
};
AudioRecord.init(options);
AudioRecord.on('data', () => {
// Dummy listener to silence "Sending 'data' with no listeners registered" warning
});
AudioRecord.start();
// Manually track duration for this chunk
setCurrentDuration(0);
clearInterval(chunkTimerRef.current);
chunkTimerRef.current = setInterval(() => {
setCurrentDuration(prev => {
const nextSec = prev + 1;
if (nextSec >= CHUNK_DURATION && !isProcessingChunk.current) {
isProcessingChunk.current = true;
clearInterval(chunkTimerRef.current);
processChunkAndContinue(currentChunkNumber); // We no longer pass filePath, AudioRecord.stop() returns it
}
return nextSec;
});
}, 1000);
} catch (err) {
console.error('Error starting next chunk:', err);
}
};
const processChunkAndContinue = async chunkNumber => {
try {
const filePath = await AudioRecord.stop();
console.log(`✅ Chunk #${chunkNumber} saved:`, filePath);
// Save the actual saved path to the array here!
setAudioArray(old => [...old, filePath]);
// Start recording next chunk IMMEDIATELY to minimize audio gap
isProcessingChunk.current = false;
startNextChunk();
// Process and upload previous chunk's file in the background
try {
const fileUrl = 'file://' + filePath.replace('file://', '');
const response = await fetch(fileUrl);
const arrayBuffer = await response.arrayBuffer();
const audioBytes = new Uint8Array(arrayBuffer); // ✅ Fast native conversion
handleUploadVoice(audioBytes, 0, session_Id, false, chunkNumber);
} catch (err) {
console.error(`Failed to process chunk #${chunkNumber}`, err);
}
} catch (err) {
console.error(`Error stopping chunk #${chunkNumber}:`, err);
}
};
I already start the next recording chunk before reading and sending the previous file, but the freeze still occurs.
Is there a problem with how I schedule chunk processing and restart the recorder in this implementation? In particular, could calling processChunkAndContinue inside the setCurrentDuration updater or registering a new audio listener for each chunk contribute to this behavior?
How should I structure this recording loop to keep the UI responsive while sending audio chunks through the socket?