如何使用 MediaStream API 录制音频

云和安全管理服务专家新钛云服 方章和翻译

Media Capture and Streams API(又名 MediaStream API)允许您从用户的麦克风录制音频,然后将录制的音频或媒体元素作为音轨获取。然后,您可以在录制后直接播放这些曲目,也可以将录制的媒体上传到您的服务器。

在本教程中,我们将创建一个网站,该网站将使用 Media Streams API 来允许用户录制某些内容,然后将录制的音频上传到服务器进行保存。用户还可以查看和播放所有上传的录音。

您可以在此 https://github.com/sitepoint-editors/mediastream-tutorial 仓库中找到本教程的完整代码。


准备一个服务器


我们将首先基于Node.js创建一个Express服务,如果您的机器上没有Node.js,请务必下载并安装它。


创建一个目录

建一个项目目录,然后切换到该目录下:

mkdir recording-tutorialcd recording-tutorial


初始化项目

然后,用npm初始化项目:

npm init -y   //选项 -y 使用默认值创建 package.json


安装需要依赖的包

接下来,我们将为我们正在创建的服务器安装 Express 并借用nodemon让其支持热重启:

npm i express nodemon


创建一个express服务

我们现在可以从创建一个简单的服务器开始。在根目录中创建index.js并填写以下代码:

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}`);
});

创建一个服务器,当前环境中如果没有启用了3000端口的话它将在3000端口上运行,并且提供了一个静态资源目录public/assets--我们将很快创建它来保存JavaScript和CSS文件以及图片。


添加脚本

最后,在package.json中的scripts下添加一个启动脚本:

"scripts": {
 "start": "nodemon index.js"
},


启动web服务

让我们来测试下创建的服务器,运行以下命令启动服务器:

npm start

服务器应从3000端口监听,你可以尝试访问localhost:3000,但你会看到一个消息说:"Cannot GET /",这是因为我们没有定义任何路由。


创建一个录音的页面

接下来,我们将创建一个主页面,用户将使用此页面记录、查看和播放录音

在public目录中创建包含以下内容的index.html文件:

<!DOCTYPE html>
<html lang="en"><head> <meta charset="UTF-8">
 <meta http-equiv="X-UA-Compatible" content="IE=edge">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Record</title>
 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/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">
     <buttonclass="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>

这个页面使用Bootstrap 5进行样式设置。目前,该页面只显示了一个用户可以用来录制的按钮。

请注意,我们正在使用图标为麦克风设计一个按钮, 你可以在Iconscout上下载图标,也可以使用GitHub存储库中的修改。

下载图标并将其放在public/assets/images中,名称为microphone.png。


添加样式

我们要引用样式表index.css,所以创建一个public/assets/css/index.css文件,内容如下。

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

.record-button:hover {
 box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important;
}


创建路由

最后,我们只需要在index.js中添加新的路由。在app.listen之前添加以下内容:

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


如果服务器还没有运行,使用npm start启动服务器,然后在浏览器中访问localhost:3000,您将看到一个记录按钮。

目前,这个按钮没有任何作用。我们需要绑定一个触发录音的单击事件。

创建文件public/assets/js/record.js并填入以下内容:

//initialize elements we'll useconst recordButton = document.getElementById('recordButton');
constrecordButtonImage = recordButton.firstElementChild;

letchunks = []; //will be used later to record audioletmediaRecorder = null; //will be used later to record audiolet audioBlob = null; //the blob that will hold the recorded audio


然后初始化一个record的函数, 绑定该函数到点击事件:

function record() {
 //TODO start recording
}

recordButton.addEventListener('click', record);

我们还将此功能作为事件监听器附加到记录按钮。


录音功能

为了开始录制,我们需要使用mediaDevices.getUserMedia()方法。

这个方法允许我们获得一个流,只有在用户同意的情况下,才能录制用户的音频和/或视频。getUserMedia方法允许我们访问本地输入设备。

getUserMedia接受一个MediaStreamConstraints对象作为参数,它包括一组约束条件,指定我们从getUserMedia获得的流中的预期媒体类型。这些约束可以是带有布尔值的音频和视频。

如果值为false,意味着用户拒绝了这个访问设备的行为。

getUserMedia返回一个承诺。如果用户允许网站进行记录,程序会收到一个MediaStream对象,我们可以用它来对用户的视频或音频流进行媒体截取。


媒体采集和流

为了使用MediaStream API对象来捕获媒体轨道,我们需要使用MediaRecorder接口。我们需要创建一个该接口的新对象,它在构造函数中接受MediaStream对象,并允许我们通过它的方法轻松地控制录音。

在记录函数中,添加以下内容:

//check if 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();
}


浏览器支持

我们首先要检查navigator.mediaDevices和navigator.mediaDevices.getUserMedia是否被定义,因为有些浏览器如Internet Explorer、Android上的Chrome或其他浏览器并不支持它。

此外,使用getUserMedia需要安全的网站,这意味着要么使用HTTPS、file://或从localhost加载的页面。所以,如果页面没有安全加载,mediaDevices和getUserMedia将无法定义。


开始录音

如果条件是false(即mediaDevices和getUserMedia都支持),我们首先将录音按钮的图片改为stop.png,你可以从Iconscout或GitHub仓库下载,并将其放在public/assets/images中。

然后,我们要检查mediaRecorder--我们在文件开头定义的--是否为空。

如果它是空的,就意味着没有正在进行的录制。所以,我们使用getUserMedia获得一个MediaStream实例来开始录制。

我们传递给它一个只有键值为audio和值为true的对象,因为我们只是在录制音频。

这就是浏览器提示用户允许网站访问麦克风的地方。如果用户允许,程序中的代码将被执行。

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


这里我们要创建一个新的MediaRecorder,把它分配给我们在文件开头定义的mediaRecorder。

我们把从getUserMedia收到的数据流传递给构造函数。然后,我们使用mediaRecorder.start()开始录制。

最后,我们将事件处理程序(我们将很快创建)与两个事件,dataavailable和stop绑定。

我们还添加了一个catch处理程序,以防用户不允许网站访问麦克风或可能抛出的任何其他异常。



停止录制

如果mediaRecorder不是空的,这一切都会发生。如果它是空的,这意味着有一个正在进行的录音,而用户正在结束它。所以,我们使用mediaRecorder.stop()方法来停止录音。

} else {
 //stop recording
 mediaRecorder.stop();
}


处理媒体录制事件

到目前为止,我们的代码在用户单击记录按钮时开始和停止记录。接下来,我们将为可用数据添加事件处理程序并停止。


当数据可用

dataavailable事件要么在完成完整的记录时被触发,要么基于传递给mediaRecorder.start()的可选参数timeslice来指示该事件应该被触发的毫秒数。传递timeslice可以对录音进行切片,并以块状形式获取。

创建mediaRecorderDataAvailable函数,该函数将处理dataavailable事件,只需将收到的BlobEvent参数中的Blob音轨添加到我们在文件开头定义的chunks数组中。

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

chunks是用来保存用户录音的音轨数组。


停止

在我们创建mediaRecorderStop(它将处理停止事件)之前,让我们首先添加HTML元素容器,它将容纳录制的音频,并有保存和丢弃按钮。

在public/index.html中的</body>标签结束前添加以下内容。

<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>


然后,在public/assets/js/record.js的开头,添加一个变量,它将是#recordedAudioContainer元素的一个Node实例。

const recordedAudioContainer = document.getElementById('recordedAudioContainer');


我们现在可以实现mediaRecorderStop。这个函数首先会删除之前录制的、没有保存的任何音频元素,创建一个新的音频媒体元素,将src设置为录制流的Blob,并显示容器。

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 = [];
}


最后,我们要把mediaRecorder和chunks重置为初始值,以处理接下来的录音。有了这段代码,我们的网站应该能够录制音频,当用户停止时,它允许他们播放录制的音频。

我们需要做的最后一件事是在index.html中链接到record.js。在body末尾添加该脚本。

<script src="/js/record.js"></script>


测试

现在让我们来看看,在你的浏览器中访问localhost:3000,点击录音按钮会询问是否允许网站使用麦克风

请确保你在本地主机或HTTPS服务器上加载网站,即使你使用的是支持的浏览器。在其他条件下,MediaDevices和getUserMedia是不可用的。

点击 "允许"。然后,麦克风的图像将变为停止的图像。同时,根据你的浏览器,你应该在地址栏看到一个录音图标。这表明麦克风目前已被网站访问。

试着录制几秒钟。然后点击停止按钮。按钮的图像将变回麦克风的图像,音频播放器将显示两个按钮--保存和取消。

接下来,我们将实现保存和取消按钮的点击事件。保存按钮应该将音频上传到服务器,而取消按钮应该将其删除。


取消按钮功能实现

我们首先要实现取消按钮的事件处理程序。点击这个按钮应该首先向用户显示一个提示,让他们确认是否要放弃录音。然后,如果用户确认了,它将移除音频播放器并隐藏按钮。

在public/assets/js/record.js的开头添加将容纳取消按钮的变量。

const discardAudioButton = document.getElementById('discardButton');
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 recorded
   resetRecording();
}
}

functionresetRecording () {
 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);

您现在测试一下,然后单击取消按钮,音频播放器将被删除,按钮会隐藏。


上传到服务器

现在,我们将实现 "保存 "按钮的点击处理程序。当用户点击保存按钮时,该处理程序将使用Fetch API将audioBlob上传至服务器。

如果你对Fetch API不熟悉,可以在我们的 "Fetch API介绍 "教程中了解更多。


让我们先在项目根目录下创建一个uploads目录:

mkdir uploads


然后,在record.js的开头,添加一个变量来保存Save按钮元素:

constsaveAudioButton = document.getElementById('saveButton');
functionsaveRecording () {
 //the form data that will hold the Blob to upload constformData = 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 recording
   resetRecording();
})
}

//add the event handler to the click event
saveAudioButton.addEventListener('click', saveRecording);

注意,一旦录音被上传,我们就用resetRecording来重置下一个录音的音频。稍后,我们将获取所有的录音,向用户展示这些录音。


创建api

我们现在需要实现上传API。这个API将把音频上传到uploads目录中。

为了在Express中轻松处理文件上传,我们将使用Multer库。Multer提供了一个处理文件上传的中间件。

运行以下程序来安装它:

npm i multer
// 在Index.js中添加以下内容const fs = require('fs');
constmulter = require('multer');

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


我们使用multer.diskStorage声明了存储,我们将其配置为在uploads目录下存储文件,并且我们将根据当前的时间戳与扩展名来保存文件。

然后,我们声明了upload,这将是上传文件的中间件。

接下来,我们要使uploads目录中的文件可以公开访问。所以,在app.listen前添加以下内容。

app.use(express.static('uploads'));


最后,我们将创建上传接口。这个接口将只是使用上传中间件来上传音频并返回一个JSON响应。

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


上传中间件将处理文件的上传。我们只需要把我们要上传的文件的字段名传递给upload.single。

请注意,通常情况下,你需要对文件进行验证,确保上传的是正确的、预期的文件类型。为了简单起见,我们在本教程中省略了这一点。


测试上传

让我们来测试一下。再次进入访问localhost:3000,录制一些东西,然后点击保存按钮。

请求上传接口,文件将被上传,并将向用户显示一个提示,通知他们录音已被保存。

你可以通过检查你项目根部的uploads目录来确认音频是否真的被上传。你应该在那里找到一个MP3音频文件。


展示上传的音频

创建相关接口:

我们要做的最后一件事是向用户展示所有的录音,以便他们可以播放。

首先,我们要创建一个接口,用来获取所有的文件。在index.js的app.listen前添加以下内容。

app.get('/recordings', (req, res) => {
 letfiles = 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 });
});


我们只是在读取uploads目录下的文件,过滤它们,只得到mp3文件,并在每个文件名后加上一个/。最后,我们将返回一个包含文件的JSON对象。

在html中展示:

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

</div>


从API中获取上传的文件:

const recordingsContainer = document.getElementById('recordings');
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';
  }
}
}


注意,在playRecording函数中,我们使用audio.paused来检查音频是否正在播放,如果音频目前没有播放,它将返回true。

我们还使用了播放和暂停的图标,这些图标将显示在每个录音中。你可以从Iconscout或GitHub资源库中获得这些图标。

当页面加载和新的录音被上传时,我们将使用fetchRecordings。

所以,在record.js的结尾和saveRecording的履行处理程序里面调用这个函数,以代替TODO注释。

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


增加相关样式:

public/assets/css/index.css

.play-button:hover {
 box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important;
}

.play-button {
 height: 8em;
 width: 8em;
 background-color: #5084d2;
}


最终测试


现在都准备好了。在你的浏览器中打开localhost:3000的网站,如果你之前上传了任何录音,你现在会看到它们。你也可以尝试上传新的,看到列表被更新。用户现在可以录制他们的声音,保存或丢弃它们。用户还可以查看所有上传的录音并播放它们。

原文:https://www.sitepoint.com/mediastream-api-record-audio/

举报
评论 0