expo: Unable to stopAndUnloadAsync a recording that just got started

Reproducible Demo

https://snack.expo.io/rJ6j67G6f

After calling await recording.startAsync(), it needs at least ~300ms before recording.stopAndUnloadAsync() can be called, otherwise it’ll fail and subsequent startAsync calls will result in

error: Only one Recording object can be prepared at a given time.

Tested on Android.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 17
  • Comments: 15 (2 by maintainers)

Most upvoted comments

For anyone who needs this fix on managed until the SDK 40 release comes out…

          try {
            await recordRef.current.stopAndUnloadAsync();
          } catch (e) {
            if (e.message.includes("Stop encountered an error: recording not stopped")) {
              await ExponentAV.unloadAudioRecorder();
              await recordRef.current._cleanupForUnloadedRecorder({durationMillis: 0} as any);
            } else {
              await handleError(e, {userMessage: "An error occurred stopping the recording."});
            }
          } finally {
            recordRef.current = undefined;
            timerStop();
            success = true;
          }

I had this issue after getting overly ES6.

const { recording: { stopAndUnloadAsync } } = this;

await stopAndUnloadAsync(); // causes error

vs

await this.recording.stopAndUnloadAsync();

This allows stopAndUnloadAsync to correctly evaluate this._canRecord.

@Aryk could you tell me more about ExponentAV.unloadAudioRecorder();?

I use await recordingInstance._cleanupForUnloadedRecorder(); and it solves the problem.

  /**
   * // TODO: fix Error: Cannot unload a Recording that has already been unloaded. || Error: Cannot unload a Recording that has not been prepared.
   * Steps to reproduce: Spam the 'Press to record the audio' button
   * Severity: Minor
   *
   * https://github.com/expo/expo/issues/1709
   */
  const recorderStop = async () => {
    await new Promise((resolve) => setTimeout(resolve, 1000)); // handle Error: Stop encountered an error: recording not stopped
    const status = await recorder.getStatusAsync();
    if (status.isRecording === true) {
      try {
        await recorder.stopAndUnloadAsync();
        console.log("recorder stopped");
      } catch (error) {
        if (
          error.message.includes(
            "Stop encountered an error: recording not stopped"
          )
        ) {
          await recorder._cleanupForUnloadedRecorder({
            canRecord: false,
            durationMillis: 0,
            isRecording: false,
            isDoneRecording: false,
          });
          console.log(`recorderStop() error handler: ${error}`);
        } else if (
          error.message.includes(
            "Cannot unload a Recording that has already been unloaded."
          ) ||
          error.message.includes(
            "Error: Cannot unload a Recording that has not been prepared."
          )
        ) {
          console.log(`recorderStop() error handler: ${error}`);
        } else {
          console.error(`recorderStop(): ${error}`);
        }
      }
    }
  };

Hi! I’ve taken a closer look at this and updated the snack to work with SDK 38 and add some additional error handling.

https://snack.expo.io/@ijzerenhein/43e79d

What appears to be going on, is that when stopAndUnloadAsync is called too quickly after start, it fails with the error recording not stopped. In the snack, if you were to hit “Record” again after that it would throw the Only one Recording object can be prepared at a given time. error because the previous recording has not been cleaned up correctly.

After digging around, it seems that the MediaRecorder class on Android will purposely cause stop to fail when the recording has not yet received any valid recording data. This article gives a good explanation: https://www.hiren.dev/2017/01/android-media-recorder-stop-failed-1007.html

  1. You called stop method too early.

According to documentation on MediaRecorder.java class it says

Stops recording. Call this after start(). Once recording is stopped, you will have to configure it again as if it has just been constructed. Note that a RuntimeException is intentionally thrown to the application, if no valid audio/video data has been received when stop() is called. This happens if stop() is called immediately after start(). The failure lets the application take action accordingly to clean up the output file (delete the output file, for instance), since the output file is not properly constructed when this happens.

That means if you start recording and in couple of seconds you call stop recording there is no significant data of recording hence stop method throws an exception. So you must wait for sometime. From my testing I found out that you should at least for minimum 10 seconds. So the solution is to have stop method called inside try catch block and handle the exception properly or do not allow user to stop recording till 10 seconds of start of recording. You can either disable stop button and make it enable after sometime. Once the exception is thrown, you should clean the the resources, release media recorder and camera and all other objects.

So in short, this behaviour is sort of by design in Android. But unfortunately expo-av is not handling this case or giving an appropriate error. We’ll have a look on how to address this.

@IjzerenHein So I can say this isn’t a bug, but the docs’ example can be improved https://docs.expo.io/versions/latest/sdk/audio/#example-2

I’m still having this issue too. Any solutions? It is something to do with the fact that its all asynchronous

hello guys, I face the same error. Did you solve the error ?

I’m facing this issue too. It says: Cannot unload a Recording that has not been prepared.