reactjstypescriptreact-hooksweb-mediarecordermediarecorder-api

React MediaRecorder audio data not being set correctly


I have this react app that records audio using the MediaRecorder API, it then takes the recorded audio data and converts it to base64. The code looks like this:

import React, { useState } from "react";

const App: React.FC = () => {
    const [recording, setRecording] = useState(false);
    const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder>();
    const [audioChunks, setAudioChunks] = useState<Blob[]>([]);

    const handleStartRecording = async () => {
        const stream = await navigator.mediaDevices.getUserMedia({
            audio: true,
        });
        const mediaRecorder = new MediaRecorder(stream);
        setMediaRecorder(mediaRecorder);
        mediaRecorder.start();
        mediaRecorder.addEventListener("dataavailable", handleDataAvailable);
        setRecording(true);
    };

    const handleStopRecording = () => {
        if (mediaRecorder) {
            mediaRecorder.stop();
            setRecording(false);
        }
    };

    const handleDataAvailable = (e: BlobEvent) => {
        if (e.data.size > 0) {
            setAudioChunks((prev) => [...prev, e.data]);
        }
    };

    const handleDownload = async () => {
        if (audioChunks) {
            const audioBlob = new Blob(audioChunks);
            convertToBase64AndSend(audioBlob);

            setMediaRecorder(undefined);
            setAudioChunks([]);
        }
    };

    const convertToBase64AndSend = (audioBlob: Blob) => {
        const reader = new FileReader();
        reader.readAsDataURL(audioBlob);
        reader.onloadend = () => {
            const base64data = reader.result as string;
            console.log(base64data);
        }
    } 

    return (
        <div>
            <button onClick={recording ? handleStopRecording : handleStartRecording}>
                {recording ? "Stop Recording" : "Start Recording"}
            </button>
            {audioChunks.length > 0 && (
                <button onClick={handleDownload}>Send Audio</button>
            )}
        </div>
    );
};

export default App;
    `

Now the code in this state works exactly as expected. You click start recording and then the audio starts recording, you click stop recording and then the audio stops recording and when you click download, it successfully converts the audio data to base64 and logs it:

enter image description here

I want to modify this code to automatically initiate the download function when I click Stop Recording. So I removed the Download button and made the handleStopRecording() function call the handleDownload() function like so:

const handleStopRecording = () => {
        if (mediaRecorder) {
            mediaRecorder.stop();
            setRecording(false);
        }

        handleDownload();
    };

return (
        <div>
            <button onClick={recording ? handleStopRecording : handleStartRecording}>
                {recording ? "Stop Recording" : "Start Recording"}
            </button>
        </div>
    );

However, now this causes an issue where the base64AudioData that I log is empty. Presumably because the audioChunks hook has not been set properly. It appears that the audioChunks hook only gets set properly when there is an additional button press, but I don't understand react well enough to know why. How can I correct this issue?

enter image description here


Solution

  • You to need to wait for the audioChunks state to be set, and since setting a state is async, and in this case it only happens after the dataavailable event is called, the chunks are not ready when you call handleDownload() right after mediaRecorder.stop().

    A simple solution would be to move the download code to a useEffect block, so it would be executed after the chunks are set:

    useEffect(() => {
      if(audioChunks.length) {
        const audioBlob = new Blob(audioChunks);
        convertToBase64AndSend(audioBlob);
    
        setMediaRecorder(undefined);
        setAudioChunks([]);
      }
    }, [audioChunks]);
    

    However, since you only need the chunks now for the download, and you reset them afterwards, you don't need the audioChunks state, and you can call the download directly from the dataavailable event handler and pass it the current chunk:

    const handleDataAvailable = (e: BlobEvent) => {
      if (e.data.size > 0) {
        const audioBlob = new Blob([e.data]);
        convertToBase64AndSend(audioBlob);
    
        setMediaRecorder(undefined);
      }
    };