Home > OS >  Canceling setTimeout early
Canceling setTimeout early

Time:03-14

I'm working on an audio recording class that either runs for an allotted period of time (such as 5 seconds) or can be stopped early by the user.

I'm using setTimeout to define the recording length, which works. However, I'm having trouble getting setTimeout working with a "stop" button. The error is as follows:

Cannot read properties of null (reading 'stop')

When the startRecording function executes, the handleStopRecording function is called which sets a timer with the "stopRecording" function. If the "stopRecording" function is called before the time elapses (by pressing the "stop" button), the function call that was initially in setTimeout will still execute when the timer expires, causing an error.

I tried fixing this by using clearTimeout, but then the "context" of the original function call is lost and we get the same error:

Cannot read properties of null (reading 'stop')

Unless I'm mistaken, I think this is an issue with closure of the setTimeout function - however I'm not sure how to clear the function early with a stop button and limit recording time.

Thank you in advance!

App.js (React.js)

import AudioRecorder from "./audioRecorder";
const App = () => {
  const [recordedNameClipURL, setRecordedNameClipURL] = useState(null);
  const [timeoutId, setTimeoutId] = useState(null);

  const recorder = new AudioRecorder();

  const startRecording = () => {
    recorder.start();
    handleStopRecording();
  };

  const handleStopRecording = async () => {
    const id = setTimeout(stopRecording, 3000);
    setTimeoutId(id);
  };

  const stopRecording = async () => {
    clearTimeout(timeoutId);

    const response = await recorder.stop();
    setRecordedNameClipURL(response);
  };

  return (
    ...
  );
};

audioRecorder.js

class AudioRecorder {
  constructor() {
    this.audioRecorder = null;
    this.audioChunks = [];
  }

  initialize = async () => {
    try {
      await this.isSupported();

      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      this.audioRecorder = new MediaRecorder(stream);

      this.audioRecorder.addEventListener("dataavailable", event => {
        this.audioChunks.push(event.data);
      });
    } catch (err) {
      console.log(err.message);
    }
  };

  start = async () => {
    try {
      await this.initialize();

      this.audioRecorder.start();
    } catch (err) {
      console.log(err.message);
    }
  };

  stop = async () => {
    try {
      this.audioRecorder.stop();
      const blob = await this.stopStream();

      return URL.createObjectURL(blob);
    } catch (err) {
      console.log(err.message);
    }
  };

  stopStream = () => {
    return new Promise(resolve => {
      this.audioRecorder.addEventListener("stop", () => {
        const audioBlob = new Blob(this.audioChunks, {
          type: this.audioRecorder.mimeType,
        });
        resolve(audioBlob);
      });
    });
  };

  isSupported = () => {
    return new Promise((resolve, reject) => {
      if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
        resolve(true);
      }
      reject(new Error("getUserMedia not supported on this browser!"));
    });
  };
}

export default AudioRecorder;

CodePudding user response:

Store the timer inside a React Ref instead

I usually store timeout/interval IDs in a React ref, because storing the handle isn't really "application state" the way that other things are. Sometimes it's needed to avoid render thrashing.

Here's what that looks like:

let timerRef = React.useRef(null)

const handleStopRecording = async () => {
  timerRef.current = setTimeout(stopRecording, 3000)
}

const stopRecording = async () => {
  clearTimeout(timerRef.current)
  timerRef.current = null // good idea to clean up your data
  
  const response = await recorder.stop()
  setRecordedNameClipURL(response)
}

Code that needs to know if the timer is running should consult the timerRef; no additional state is needed:

let timerIsRunning = !!timerRef.current

CodePudding user response:

You can try using a boolean value to check if the process is stopped. You can store it in state and change its value when starting or stopping

  const [isStopped, setIsStopped] = useState(false);

  const handleStopRecording = async () => {
    const id = setTimeout(() => {
      if(!isStopped){
        stopRecording
      }
    }, 3000);
    setTimeoutId(id);
  };
  • Related