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:

  1. Initialize the recorder and start recording.

  2. After 5 seconds (CHUNK_DURATION = 5), stop recording and get the saved file path.

  3. Start recording the next chunk immediately.

  4. 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?

0
Oct 8 at 1:21 PM
User AvatarDawood zia
#android#reactjs#react-native#websocket#audio-recording

No answer found for this question yet.