Description
Describe the bug
When using react-voice-visualizer purely for the waveform (i.e., handling the actual recording/MediaRecorder in my own code), the internal microphone stream opened by the library’s startRecording() call continues running and is not released even after calling stopRecording() and unmounting the component. This leads to a stuck audio input device in the browser (and sometimes a TypeError: Cannot read properties of null (reading 'getByteTimeDomainData') if the component is unmounted mid‐recording).
To Reproduce
- Integrate react-voice-visualizer with a custom hook that also uses MediaRecorder.
- Call controls.startRecording() on the library to visualize the waveform.
- Attempt to stop both the library’s recording and the custom recorder.
- Unmount or hide the component.
- Observe that the mic input remains active, or a TypeError occurs in the console.
Expected behavior
Calling controls.stopRecording() (and subsequently unmounting the component) should close/stop any and all audio streams cleanly, freeing the microphone in the browser and preventing any null reference errors when the component unmounts.
Screenshots
(Not strictly applicable here, but the console error message is TypeError: Cannot read properties of null (reading 'getByteTimeDomainData') when unmounting mid‐recording.)
Package info (please complete the following information):
"react-voice-visualizer": "^2.0.8"
Additional context
- I need the library only for real-time waveform visualization, while my custom hook handles the actual audio recording logic.
- The library’s second internal recorder conflicts with the custom recorder, and does not appear to close or release the microphone if unmounted prematurely.
- A potential solution is to support an external stream (e.g., letting users pass an existing MediaStream to the visualizer) so the library doesn’t open its own mic by default.
CODE
'use client';
import React, { useEffect } from 'react';
import { useVoiceVisualizer, VoiceVisualizer } from 'react-voice-visualizer';
interface MicVoiceVisualizerProps {
/** True when capturing mic input */
isRecording: boolean;
/** Height (px) of the waveform */
containerHeight?: number;
/**
* Visible duration for the rolling window.
* (react-voice-visualizer handles scaling internally.)
*/
continuousWaveformDuration?: number;
/** Color for the inactive (background) waveform */
waveColor?: string;
/** Color for the active (recorded) waveform bars */
progressColor?: string;
/** Background color of the waveform container */
backgroundColor?: string;
/** Bar styling: width (px) of each bar */
barWidth?: number;
/** Gap (px) between bars */
barGap?: number;
/** Corner radius (px) of each bar */
barRadius?: number;
}
export default function MicVoiceVisualizer({
isRecording,
containerHeight = 40,
continuousWaveformDuration = 30, // Not directly used here—but you might later use it to configure scaling
waveColor = '#dddddd',
progressColor = '#555555',
backgroundColor = 'transparent',
barWidth = 8,
barGap = 2,
barRadius = 5,
}: MicVoiceVisualizerProps) {
// Initialize the visualizer controls via the provided hook.
const controls = useVoiceVisualizer({
onStopRecording: () => {},
});
const { isRecordingInProgress } = controls;
// When isRecording changes, start or stop recording.
useEffect(() => {
if (isRecording && !isRecordingInProgress) {
console.log('MicVoiceVisualizer: Starting recording');
controls.startRecording?.();
} else if (!isRecording && isRecordingInProgress) {
console.log('MicVoiceVisualizer: Stopping recording');
controls.stopRecording?.();
}
}, [isRecording, controls, isRecordingInProgress]);
return (
<VoiceVisualizer
controls={controls}
width="100%"
height={containerHeight}
backgroundColor={backgroundColor}
// Mapping: we’ll use progressColor for the main (active) waveform and waveColor for the background (inactive)
mainBarColor={progressColor}
secondaryBarColor={waveColor}
// We hide the default UI since you have your own controls.
isDefaultUIShown={false}
// Other props (barWidth, gap, rounded) can be passed if supported:
barWidth={barWidth}
gap={barGap}
rounded={barRadius}
/>
);
}