How do I record audio using the MediaStream API

The original link: www.sitepoint.com/mediastream…

The Media Capture and Streams API allows you to record audio from a user’s device and then retrieve the audio track. You can play the recorded audio directly or upload it to the server.

In this tutorial, we will create a website that uses the Media Streams API to allow users to record audio and upload it to the server for saving. Users can also view and play uploaded audio.

The source code is on Github.

Setting up a server

First we create a server using Node.js and Express. So, the first step is to download Node.js, if you don’t have it on your computer.

Create a directory:

Create a directory to hold the project, and then go to the directory

mkdir recording-tutorial
cd recording-tutorial
Copy the code

Initialize the project

Initialize the project with NPM:

npm init -y
Copy the code

The -y argument will create a package.json file with the default value

Install dependencies

Next, we rely on Express and Nodemon, whose role is to restart the server when the file is modified.

npm i express nodemon
Copy the code

Creating an Express Server

We’ll start by creating a simple server. Create the index.js file in the project root directory and enter the following

const path = require('path');
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

app.use(express.static('public/assets'));

app.listen(port, () = > {
  console.log(`App listening at http://localhost:${port}`);
});
Copy the code

The above code creates a server that runs on port 3000 (if port 3000 is not occupied) and uses the directory Public/Assets as a static resource server, where we’ll put JavaScript,CSS, images, etc.

Add a script

Finally, add a command called start in the scripts section of the package.json file:

"scripts": {
  "start": "nodemon index.js"
},
Copy the code

Start the Web server Start the web server

Let’s test this by starting the server with the following command:

npm start
Copy the code

The server will start on port 3000. You can access it by typing localhost:3000 in the browser address bar, but you will receive the message “Cannot GET/” because we haven’t defined any routes yet.

Creating a Recording page

Next, we’ll create a home page that users can record and preview.

Create a public directory and create an index.html file in the public directory by typing the following:

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta >
  <title>Record</title>
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
  <link href="/css/index.css" rel="stylesheet" />
</head>
<body class="pt-5">
  <div class="container">
    <h1 class="text-center">Record Your Voice</h1>
    <div class="record-button-container text-center mt-5">
      <button class="bg-transparent border btn record-button rounded-circle shadow-sm text-center" id="recordButton">
        <img src="/images/microphone.png" alt="Record" class="img-fluid" />
      </button>
    </div>
  </div>
</body>
</html>
Copy the code

The page will be laid out using Bootstrap 5. But for now, there is only one button on the page for users to record.

Notice that we’re using an image of the microphone. You can download images from Iconscout or you can use the GitHub Repository image above.

Download the image and name it “microphone. PNG” and place it under the public/ Assets /images directory.

Add the style

We put style in the index. The CSS, so to create a public/assets/CSS/index. The CSS file and type the following contents:

.record-button {
  height: 8em;
  width: 8em;
  border-color: #f3f3f3 ! important;
}

.record-button:hover {
  box-shadow: 0 .5rem 1rem rgba(0.0.0.15)! important;
}
Copy the code

Create routing

Finally, we add a route to index.js. Add the following code before app.listen:

app.get('/'.(req, res) = > {
  res.sendFile(path.join(__dirname, 'public/index.html'));
});
Copy the code

If the server is not started, start the service with NPM start and then access localhost:3000 in your browser. The page is as follows:

Now the button doesn’t do anything, we need to bind a click event to record.

Create a public/assets/js/record. Js file add the following content:

//initialize elements we'll use
const recordButton = document.getElementById('recordButton');
const recordButtonImage = recordButton.firstElementChild;

let chunks = []; //will be used later to record audio
let mediaRecorder = null; //will be used later to record audio
let audioBlob = null; //the blob that will hold the recorded audio
Copy the code

Let’s initialize some variables that we’ll need later. Then create a record function that listens for events triggered by clicking a recordButton.

function record() {
  //TODO start recording
}

recordButton.addEventListener('click', record);
Copy the code

Recording media

In order to start recording, we need to use mediaDevices. GetUserMedia () method.

This method allows us to capture and record audio and video streams as long as the user has given corresponding permissions on the site. GetUserMedia allows us to access the local input device.

GetUserMedia takes as a parameter a MediaStreamConstraints object that contains a set of constraints specifying the type of media to record. These constraints can be audio or video with Boolean values, such as {audio:true,video:true}.

If a value is false, the corresponding device is not accessed to record the corresponding media stream.

GetUserMedia returns a Promise. If the user agrees to record on the web, the Promise will resolve and accept a MediaStream object that holds the stream of audio and video we captured.

Capturing a media stream

To record a MediaStream using the MediaStream API, we need to use the MediaRecorder interface. We need to create a new interface object that accepts the MediaStream object in the constructor and allows us to easily control recording through its methods.

Inside the Record function, add the following code:

// Check whether the browser supports getUserMedia
if(! navigator.mediaDevices || ! navigator.mediaDevices.getUserMedia) { alert('Your browser does not support recording! ');
  return;
}

// browser supports getUserMedia
// change image in button 
recordButtonImage.src = `/images/${mediaRecorder && mediaRecorder.state === 'recording' ? 'microphone' : 'stop'}.png`;
if(! mediaRecorder) {// Start recording
  navigator.mediaDevices.getUserMedia({
    audio: true,
  })
    .then((stream) = > {
      mediaRecorder = new MediaRecorder(stream);
      mediaRecorder.start();
      mediaRecorder.ondataavailable = mediaRecorderDataAvailable;
      mediaRecorder.onstop = mediaRecorderStop;
    })
    .catch((err) = > {
      alert(`The following error occurred: ${err}`);
      // change image in button
      recordButtonImage.src = '/images/microphone.png';
    });
} else {
  // stop recording
  mediaRecorder.stop();
}
Copy the code

Browser support

We first check the navigator. MediaDevices and navigator. MediaDevices. Whether getUserMedia definition, since some browsers such as Internet Explorer, Chrome on Android does not support these objects.

In addition, using getUserMedia must be a secure site, which means either using HTTPS,file:// loaded pages, or from localhost. Otherwise, mediaDevices and getUserMedia will be undefined.

Start recording

If the condition is false (mediaDevices and getUserMedia are both supported), we first change the image of the record button to stop.png. You can download images from Iconscout or the GitHub Repository and place them in the public/ Assets /images directory.

Then, we check if mediaRecorder is null, which is the variable we defined at the beginning of the file.

If null, no recording is in progress. We then use getUserMedia to get an instance of MediaStream to start recording.

Pass {audio:true} as an argument, since we only need to record the audio.

The browser prompts the user to allow the microphone, and if the user does, the promise returned by getUserMedia will be fullfilled, and the code will execute.

mediaRecorder = new MediaRecorder(stream);
mediaRecorder.start();
mediaRecorder.ondataavailable = mediaRecorderDataAvailable;
mediaRecorder.onstop = mediaRecorderStop;
Copy the code

Above we create the MediaRecorder instance and assign the value to the MediaRecorder variable created earlier.

We pass the data received from getUserMedia to the constructor, and we use mediaRecorder.start() to start recording.

Finally, we bind event handlers for dataavailable and Stop, which we will create later.

We also added catch handling logic to prevent situations where the user is not allowed to use the microphone or other exceptions thrown.

To stop recording

The following code is executed when mediaRecorder is not null. If null, it means that one is recording and the user is ending it. So, we use the mediaRecorder.stop() method to end the recording.

} else {
  //stop recording
  mediaRecorder.stop();
}
Copy the code

Process media recording events

So far, our code has enabled the user to hit record and pause recording. The next. We’ll add event handling for dataavailable and Stop.

On data available

Dataavailable is triggered when recording is complete and when mediaRecorder.start() is called to start recording when the timeslice parameter is passed, which indicates how often the event is triggered. Passing timeslice allows the recording to be sharded and partitioned for data.

Create mediaRecorderDataAvailable function, it will handle dataavailable events, just will receive BlobEvent tracks of Blob parameters added to at the beginning of the file we define chunks in the array.

function mediaRecorderDataAvailable(e) {
  chunks.push(e.data);
}
Copy the code

The chunk will be an array of tracks for user recordings.

On stop

Before creating a mediaRecorderStop that handles stop events, let’s first add the HTML element container that will hold recorded audio and the buttons Save and Discard.

Add the following to public/index.html, before the final tag.

<div class="recorded-audio-container mt-5 d-none flex-column justify-content-center align-items-center"
  id="recordedAudioContainer">
  <div class="actions mt-3">
    <button class="btn btn-success rounded-pill" id="saveButton">Save</button>
    <button class="btn btn-danger rounded-pill" id="discardButton">Discard</button>
  </div>
</div>
Copy the code

Then, in the public/assets/js/record. The beginning of the js, add a variable, it will be # recordedAudioContainer elements of a node instance.

const recordedAudioContainer = document.getElementById('recordedAudioContainer');
Copy the code

Now we can implement mediaRecorderStop. This function first deletes previously recorded, unsaved audio elements, then creates a new audio media element, sets SRC to the Blob of the recorded stream, and displays the container.

function mediaRecorderStop () {
  //check if there are any previous recordings and remove them
  if (recordedAudioContainer.firstElementChild.tagName === 'AUDIO') {
    recordedAudioContainer.firstElementChild.remove();
  }
  //create a new audio element that will hold the recorded audio
  const audioElm = document.createElement('audio');
  audioElm.setAttribute('controls'.' '); //add controls
  //create the Blob from the chunks
  audioBlob = new Blob(chunks, { type: 'audio/mp3' });
  const audioURL = window.URL.createObjectURL(audioBlob);
  audioElm.src = audioURL;
  //show audio
  recordedAudioContainer.insertBefore(audioElm, recordedAudioContainer.firstElementChild);
  recordedAudioContainer.classList.add('d-flex');
  recordedAudioContainer.classList.remove('d-none');
  //reset to default
  mediaRecorder = null;
  chunks = [];
}
Copy the code

Finally, we need to reset the mediaRecorder and Chunks to their initial values to process the next recording. With this code, our site should be able to record audio, allowing the user to play the recorded audio when they stop recording.

The last thing we need to do is introduce record.js in index.html. Add “script” to the end of “body “.

<script src="/js/record.js"></script>
Copy the code

The test record

Now let’s test it. Go to Localhost :3000 in your browser and click the Record button. You will be asked to allow the site to use a microphone.

Make sure you load the site from a localhost or HTTPS server, even if you’re using a supported browser. Otherwise,MediaDevicesandgetUserMediaWill not be available.

Click Permit. The microphone image will then change to a stopped image. At the same time, you will see a recording icon in the browser address bar. This indicates that the microphone is currently accessed by the website.

Try recording for a few seconds. Then click the stop button. The image of the button changes back to the image of the microphone, and two buttons –Save and Discard — are displayed below the audio player.

Next, we will implement click events for the Save and Discard buttons. The Save button should upload the audio to the server, while the Discard button should delete it.

Discard Click event processing

We’ll start by implementing an event handler for the Discard button. Clicking this button should first display a prompt to the user asking them to confirm whether they want to abandon the recording. If the user confirms, it will remove the audio player and hide the button.

Add the variable that holds the Discard button to the beginning of the following code

const discardAudioButton = document.getElementById('discardButton');
Copy the code

Add the following code to the end of the file:

function discardRecording () {
  //show the user the prompt to confirm they want to discard
  if (confirm('Are you sure you want to discard the recording? ')) {
    //discard audio just recordedresetRecording(); }}function resetRecording () {
  if (recordedAudioContainer.firstElementChild.tagName === 'AUDIO') {
    //remove the audio
    recordedAudioContainer.firstElementChild.remove();
    //hide recordedAudioContainer
    recordedAudioContainer.classList.add('d-none');
    recordedAudioContainer.classList.remove('d-flex');
  }
  //reset audioBlob for the next recording
  audioBlob = null;
}

//add the event listener to the button
discardAudioButton.addEventListener('click', discardRecording);
Copy the code

You can now try recording something and hit the Discard button. The audio player is removed and the buttons are hidden.

Uploading to the server

Save event handling

We will now implement the click handler for the Save button. When the user clicks the Save button, the handler uses the Fetch API to upload the audioBlob to the server.

If you are not familiar with the Fetch API, you can learn more about it in our “Introduction to the Fetch API” tutorial.

We create a uploads directory in the project root directory.

mkdir uploads
Copy the code

Then, at the beginning of record.js, add a variable to hold the “Save” button element.

const saveAudioButton = document.getElementById('saveButton');
Copy the code

Then, at the end of the file, add the following code:

function saveRecording () {
  //the form data that will hold the Blob to upload
  const formData = new FormData();
  //add the Blob to formData
  formData.append('audio', audioBlob, 'recording.mp3');
  //send the request to the endpoint
  fetch('/record', {
    method: 'POST'.body: formData
  })
  .then((response) = > response.json())
  .then(() = > {
    alert("Your recording is saved");
    //reset for next recording
    resetRecording();
    //TODO fetch recordings
  })
  .catch((err) = > {
    console.error(err);
    alert("An error occurred, please try again later");
    //reset for next recordingresetRecording(); })}//add the event handler to the click event
saveAudioButton.addEventListener('click', saveRecording);
Copy the code

Note that once a recording has been uploaded, we use resetRecording to reset the audio for the next recording. Later, we’ll take all the recordings and show them to the user.

Create API

We now need to implement the corresponding interface to upload the audio to the Uploads directory.

To handle file uploads in Express, we will use the library Multer. It provides a middleware that handles file uploads.

We use NPM to install it.

npm i multer
Copy the code

Next, in index.js, add the following code at the beginning of the file

const fs = require('fs');
const multer = require('multer');

const storage = multer.diskStorage({
  destination(req, file, cb) {
    cb(null.'uploads/');
  },
  filename(req, file, cb) {
    const fileNameArr = file.originalname.split('. ');
    cb(null.`The ${Date.now()}.${fileNameArr[fileNameArr.length - 1]}`); }});const upload = multer({ storage });
Copy the code

We declare storage with multer.diskStorage and configure it to store files under uploads, which we will save according to the current timestamp and extension.

Then, we declare an upload, which will be the middleware for uploading files.

Next, we want to make the files under the Uploads directory publicly accessible. Therefore, add the following to app.listen.

app.use(express.static('uploads'));
Copy the code

Finally, we’ll create the upload processing logic, all we need to do is use the Upload middleware to upload the audio and return a JSON response.

app.post('/record', upload.single('audio'), (req, res) = > res.json({ success: true }));
Copy the code

Upload Middleware will handle file uploads. We just need to pass the field name of the file we want to upload to upload.single.

Note that in general, you need to perform validation on Files and make sure that you are uploading the correct, expected file type. But for the sake of simplicity, we’ve omitted this point in this tutorial.

Test the upload

Let’s test that out. Go to localhost:3000 again in your browser, record something, and hit the Save button.

The request will be sent to the back end, the file will be uploaded, and a notification will be displayed to the user informing them that the recording has been saved.

You can check to see if the audio was uploaded successfully by checking the Uploads directory in your project root directory. You should find an MP3 audio file there.

Display all recordings

Create the interface

The last thing we need to do is show the user all the recordings so they can play them back.

First, we need to create an interface to get all the files. Add the following to app.listen in index.js:

app.get('/recordings'.(req, res) = > {
  let files = fs.readdirSync(path.join(__dirname, 'uploads'));
  files = files.filter((file) = > {
    // check that the files are audio files
    const fileNameArr = file.split('. ');
    return fileNameArr[fileNameArr.length - 1= = ='mp3';
  }).map((file) = > ` /${file}`);
  return res.json({ success: true, files });
});
Copy the code

All we do is read the files in the Uploads directory, filter them, just get the MP3 files, and prefix each file name with a slash. Finally, we return a JSON object containing a list of files.

Adding a recording container

Next, we’ll add an HTML element that will be the container for the recording we want to present. At the end of the body, add the following before the record.js script.

<h2 class="mt-3">Saved Recordings</h2>
<div class="recordings row" id="recordings">

</div>
Copy the code

Obtain the uploaded file through the API

Also add a variable at the beginning of record.js to store the #recordings element.

const recordingsContainer = document.getElementById('recordings');
Copy the code

We’ll then add a fetchRecordings function that calls the interface we created earlier, and then render the data into an audio player via the createRecordingElement function.

We’ll also add a playRecording event listener that plays the click events of the audio button.

Add the following at the end of record.js.

function fetchRecordings () {
  fetch('/recordings')
  .then((response) = > response.json())
  .then((response) = > {
    if (response.success && response.files) {
      //remove all previous recordings shown
      recordingsContainer.innerHTML = ' ';
      response.files.forEach((file) = > {
        //create the recording element
        const recordingElement = createRecordingElement(file);
        //add it the the recordings container
        recordingsContainer.appendChild(recordingElement);
      })
    }
  })
  .catch((err) = > console.error(err));
}

//create the recording element
function createRecordingElement (file) {
  //container element
  const recordingElement = document.createElement('div');
  recordingElement.classList.add('col-lg-2'.'col'.'recording'.'mt-3');
  //audio element
  const audio = document.createElement('audio');
  audio.src = file;
  audio.onended = (e) = > {
    //when the audio ends, change the image inside the button to play again
    e.target.nextElementSibling.firstElementChild.src = 'images/play.png';
  };
  recordingElement.appendChild(audio);
  //button element
  const playButton = document.createElement('button');
  playButton.classList.add('play-button'.'btn'.'border'.'shadow-sm'.'text-center'.'d-block'.'mx-auto');
  //image element inside button
  const playImage = document.createElement('img');
  playImage.src = '/images/play.png';
  playImage.classList.add('img-fluid');
  playButton.appendChild(playImage);
  //add event listener to the button to play the recording
  playButton.addEventListener('click', playRecording);
  recordingElement.appendChild(playButton);
  //return the container element
  return recordingElement;
}

function playRecording (e) {
  let button = e.target;
  if (button.tagName === 'IMG') {
    //get parent button
    button = button.parentElement;
  }
  //get audio sibling
  const audio = button.previousElementSibling;
  if (audio && audio.tagName === 'AUDIO') {
    if (audio.paused) {
      //if audio is paused, play it
      audio.play();
      //change the image inside the button to pause
      button.firstElementChild.src = 'images/pause.png';
    } else {
      //if audio is playing, pause it
      audio.pause();
      //change the image inside the button to play
      button.firstElementChild.src = 'images/play.png'; }}}Copy the code

Note that in playRecording, we use Audio. Paused to check if the audio is playing, and if the audio is not playing, Audio. Paused returns true.

We also used play and pause ICONS, which are displayed in each recording. You can get these ICONS from Iconscout or GitHub repositories.

When the page loads and a new recording is uploaded, we’ll call fetchRecordings.

Therefore, this function is called at the end of Record.js and in saveRecording’s Promise handler to replace the TODO annotation.

.then(() = > {
  alert("Your recording is saved");
  //reset for next recording
  resetRecording();
  //fetch recordings
  fetchRecordings();
})
Copy the code

Add the style

The last thing we need to do is add some style to the element we are creating. In the public/assets/CSS/index. The CSS to add the following content.

.play-button:hover {
  box-shadow: 0 .5rem 1rem rgba(0.0.0.15.)! important; } .play-button {height: 8em;
  width: 8em;
  background-color: #5084d2;
}
Copy the code

Test all features

Everything is ready. Open the localhost:3000 page in your browser, and if you uploaded any recordings before, you can see them now. You can also try uploading new recordings and see the list updated.

Users can now record their sounds and save or discard them. Users can also view all uploaded recordings and play them back.

conclusion

The MediaStream API allows us to add media capabilities to our users, such as recording audio. In addition, the MediaStream Web API allows us to record videos, screen captures, and more. Follow the information in this tutorial, along with tutorials from MDN and SitePoint, to add other media features to your site.