Microphone icon persists after stopping and disposing audio track

Video SDK Type and Version
1.11.6

Description
When starting an audio preview using the following code, the microphone icon appears in the browser tab bar:

      this.localAudioInputTrack = ZoomVideo.createLocalAudioTrack(deviceId);
      await this.localAudioInputTrack.start();

However, when handling page disposal (e.g., navigating to a new tab or returning to the previous tab that does not use the microphone), I attempt to stop the audio track using the code below. Despite this, the audio track does not seem to be disposed of, and the microphone icon remains visible on the browser tab bar:

        await this.localAudioInputTrack.stop();
        this.localAudioInputTrack = null;

Thank you for your assistance!

Error?
No error was throwed.

Environments
OS: MacOS 15.1
Browser: Chrome 131.0.6778.205

Hey @lmtruong1512

Thanks for your feedback.

this.localAudioInputTrack = ZoomVideo.createLocalAudioTrack(deviceId);
await this.localAudioInputTrack.start();
await this.localAudioInputTrack.stop();
this.localAudioInputTrack = null;

We used the same approach for testing, and it worked as expected—the microphone icon turns off after calling the stop method.

Could you help verify whether the getUserMedia method is called elsewhere in the code? For instance, if the localAudioInputTrack.testMicrophone method is used without being destroyed after testing, it could cause this behavior.

Thanks
Vic

Thanks for your help @vic.yang ,
The getUserMedia method is used in multiple places throughout our code. One of those usages includes the testMicrophone method, but I destroyed it after use. However, the microphone icon still appears.

Use:

      this.micLevelRef = this.localAudioInputTrack.testMicrophone({
        microphoneId: deviceId,
        onAnalyseFrequency: (value) => {
          this.setMicLevel(Math.min(100, value));
        },
      });

Dispose:

        this.micLevelRef.stop();
        this.micLevelRef.destroy();
        this.micLevelRef = undefined;

Suppose there are many other places that use getUserMedia and in our client code we also use them, how can we ensure that all instances are properly disposed of to release the microphone completely?

Hey @lmtruong1512

Thanks for sharing the context.

this.micLevelRef.stop();
this.micLevelRef.destroy();
this.micLevelRef = undefined;

After calling the stop method, the microphone will be released.

For other parts of the code, it is recommended to obtain the stream instance returned by getUserMedia. Once it is no longer needed, iterate through the tracks and stop them to ensure proper resource cleanup.

audioStream.getAudioTracks().forEach((audioTrack) => audioTrack.stop())

Thanks
Vic

1 Like

Thanks for your help @vic.yang ,
I noticed that calling stop, destroy, and setting the object to undefined doesn’t fully release the microphone. When I remove testMicrophone and other parts that use getUserMedia, the microphone is completely released, and the icon disappears from the tab bar. However, if I keep testMicrophone, the microphone icon still appears on the tab bar, even after using the methods mentioned above. Could you please check this again? How can I fully release the microphone after using testMicrophone?

this.micLevelRef.stop();
this.micLevelRef.destroy();
this.micLevelRef = undefined;

@vic.yang Can you check it? Thank you very much for your help!

Hey @lmtruong1512

Sorry for the late reply.

this.micLevelRef.stop();
this.micLevelRef.destroy();
this.micLevelRef = undefined;

After testing several times, the stop method indeed releases the microphone.

However, we identified an issue in our sample application related to the behavior you described. Specifically, in the following line:

if (!isPlayingRecording && !isRecordingVoice) {
  microphoneTesterRef.current = localAudio.testMicrophone({
    microphoneId: activeMicrophone,
    speakerId: activeSpeaker,
    recordAndPlay: true,
    onAnalyseFrequency: (value) => {
      setInputLevel(Math.min(100, value));
    },
    ...
  });
}

If microphoneTesterRef.current retains a previous instance of testMicrophone without clearing it (by calling the stop and destroy methods) before invoking testMicrophone again, the previous instance will continue to occupy the microphone. The following code will fix the issue:

if (!isPlayingRecording && !isRecordingVoice) {
// stop and destroy the previous instance
  if (microphoneTesterRef.current && microphoneTesterRef.current?.stop) {
    microphoneTesterRef.current?.stop();
    microphoneTesterRef.current?.destroy();
  }
  microphoneTesterRef.current = localAudio.testMicrophone({
    microphoneId: activeMicrophone,
    speakerId: activeSpeaker,
    recordAndPlay: true,
    onAnalyseFrequency: (value) => {
      setInputLevel(Math.min(100, value));
    },
    ...
  });
}

Thanks
Vic

1 Like

@vic.yang Thankyou for your help!
I tried destroying the previous microphoneTesterRef instance and stopping the localAudioInputTrack before starting. However, on the first attempt, it takes approximately 30 seconds to release instead of releasing immediately. But subsequent attempts release the resources right away. It would be great if this issue is resolved in the next release, rather than relying on workarounds like this.

Thanks