Record plugin
The Record plugin lets you capture audio from the user’s microphone and display a live waveform preview while recording. See the live demo to try it in your browser.
Setup
Install wavesurfer.js (if you haven’t already — see Getting started), then import and register the plugin:
import WaveSurfer from 'wavesurfer.js'
import RecordPlugin from 'wavesurfer.js/dist/plugins/record.js'
const ws = WaveSurfer.create({
container: '#waveform',
})
const record = ws.registerPlugin(RecordPlugin.create({
renderRecordedAudio: true,
}))
CDN / UMD:
<script src="https://unpkg.com/wavesurfer.js@7"></script>
<script src="https://unpkg.com/wavesurfer.js@7/dist/plugins/record.js"></script>
<script>
const ws = WaveSurfer.create({ container: '#waveform' })
const record = ws.registerPlugin(WaveSurfer.RecordPlugin.create())
</script>
Options
| Option | Type | Default | Description |
|---|---|---|---|
renderRecordedAudio |
boolean |
true |
Load and display the recorded audio into the waveform when recording ends. |
scrollingWaveform |
boolean |
false |
Show a scrolling live waveform during recording (fixed-window, new data enters from the right). |
scrollingWaveformWindow |
number |
5 |
Width of the scrolling window in seconds. |
continuousWaveform |
boolean |
false |
Accumulate and grow the waveform from left to right as audio is recorded. |
continuousWaveformDuration |
number |
— | Pre-allocate the waveform for this many seconds. Omit to size it to the container width. |
mimeType |
string |
auto-detected | MIME type passed to MediaRecorder (e.g. 'audio/webm'). Falls back to the first browser-supported type. |
audioBitsPerSecond |
number |
128000 |
Encoding bitrate. The default avoids variable-bitrate encoding. |
mediaRecorderTimeslice |
number |
— | Interval in milliseconds at which MediaRecorder delivers data chunks. Drives record-data-available. |
Recording
Starting and stopping
// Start recording (requests mic permission if needed)
await record.startRecording()
// Stop recording; fires 'record-end' with a Blob
record.stopRecording()
startRecording() is async — it requests microphone access if a stream isn’t already open. Once called, recording begins immediately and the live waveform preview (if configured) activates.
stopRecording() finalises the recording and emits the record-end event with the complete audio as a Blob.
Pausing and resuming
record.pauseRecording() // pauses the MediaRecorder and the live waveform
record.resumeRecording() // resumes both
State checks
record.isRecording() // true while actively capturing (not paused)
record.isPaused() // true while paused
Handling the recorded audio
record.on('record-end', (blob) => {
// blob is a Blob — create an object URL to download or play it
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'recording.' + blob.type.split('/')[1]
a.click()
})
If renderRecordedAudio is true (the default), the plugin automatically loads the blob into the waveform when recording ends, so users can play it back straight away.
Choosing a microphone
Call the static helper to enumerate audio input devices, then pass deviceId to startRecording():
// Note: device labels are only populated after the user has granted
// microphone permission (e.g. after the first startRecording() call).
const devices = await RecordPlugin.getAvailableAudioDevices()
// devices is an array of MediaDeviceInfo objects
const select = document.querySelector('#mic-select')
devices.forEach((device) => {
const option = document.createElement('option')
option.value = device.deviceId
option.text = device.label || device.deviceId
select.appendChild(option)
})
// Pass the chosen deviceId when starting
select.addEventListener('change', async () => {
await record.startRecording({ deviceId: select.value })
})
startRecording() accepts any MediaTrackConstraints — deviceId is the most common one, but you can also pass echoCancellation, noiseSuppression, sampleRate, etc.
Live waveform
The plugin offers two modes for displaying audio while recording. Both are disabled by default; enable exactly one.
Scrolling waveform
RecordPlugin.create({
scrollingWaveform: true,
scrollingWaveformWindow: 5, // show the last 5 seconds
})
The waveform acts like a moving ticker tape — the oldest data scrolls off the left edge and new data arrives on the right. The window is fixed; the waveform does not grow. This is the simplest live-preview mode.
Continuous waveform
RecordPlugin.create({
continuousWaveform: true,
continuousWaveformDuration: 120, // pre-allocate 2 minutes
})
The waveform grows from left to right as audio is recorded, similar to how a voice memo app displays input. A cursor follows the recording head. If continuousWaveformDuration is omitted the plugin sizes the buffer to match the container’s pixel width.
Waveform flicker during recording. Without scrollingWaveform or continuousWaveform enabled, the plugin re-renders raw time-domain data on every frame (100 fps), which causes noticeable flicker. Enable one of the two live-waveform modes to eliminate this: scrollingWaveform normalises amplitude and uses peak values for stable rendering; continuousWaveform accumulates data so only the rightmost column changes per frame.
Only enable one live mode at a time. Enabling both scrollingWaveform and continuousWaveform together is not a documented configuration and produces undefined rendering behaviour.
Output format
The recording is delivered as a Blob whose MIME type is determined by mimeType. If you don’t set mimeType, the plugin tries audio/webm, audio/wav, audio/mpeg, audio/mp4, and audio/mp3 in order, picking the first one the browser supports.
RecordPlugin.create({
mimeType: 'audio/webm',
audioBitsPerSecond: 128000,
})
Converting to WAV. Most browsers produce audio/webm (with Opus codec) rather than WAV, even if you request audio/wav. True PCM WAV output requires post-processing: decode the blob with the Web Audio API (AudioContext.decodeAudioData), then re-encode the raw AudioBuffer into a WAV container using a library such as audiobuffer-to-wav or lamejs. There is no built-in WAV encoder in the plugin.
Events
Subscribe on the record plugin instance:
| Event | Payload | When |
|---|---|---|
record-start |
[] |
Recording begins (after startRecording() resolves). |
record-pause |
blob: Blob |
Recording is paused. The blob contains audio captured so far. |
record-resume |
[] |
Recording resumes after a pause. |
record-end |
blob: Blob |
Recording stops. The blob is the complete recording. |
record-progress |
duration: number |
Fires continuously (~100 fps) with elapsed time in milliseconds. |
record-data-available |
blob: Blob |
Fires each time MediaRecorder delivers a chunk (controlled by mediaRecorderTimeslice). |
record.on('record-progress', (durationMs) => {
const seconds = Math.floor(durationMs / 1000)
const minutes = Math.floor(seconds / 60)
timer.textContent = `${minutes}:${String(seconds % 60).padStart(2, '0')}`
})
record.on('record-end', (blob) => {
console.log('Recorded', blob.size, 'bytes as', blob.type)
})
Common pitfalls
Microphone stays “in use” after stopping. Calling stopRecording() stops the MediaRecorder but does not automatically release the microphone stream. The browser’s recording indicator stays on. To fully release the mic, call record.stopMic() after stopping:
record.stopRecording()
record.stopMic()
Recording quality with simultaneous playback. Playing audio through the same page while recording a microphone will cause the output to bleed into the capture on devices without hardware echo cancellation. Pass { echoCancellation: true, noiseSuppression: true } to startRecording() and avoid playing audio during capture on mobile.
Mobile / iOS device capture. Safari on iOS requires a user gesture (button tap) to both open the AudioContext and start getUserMedia. Call startRecording() directly inside a click handler — do not await anything before it in a chain that started outside a gesture. Some iOS versions also restrict which MIME types are accepted; if recording fails silently, leave mimeType unset so the plugin auto-detects the best available format.
Recording stops after ~1 second in React. This is typically caused by a component re-render that destroys and re-creates the wavesurfer / plugin instance mid-recording. Create the wavesurfer instance inside a useRef or useEffect with an empty dependency array so it is created once and persists across renders. See Frameworks for integration patterns.