[译]如何使用MediaStream API录制音频

[译]如何使用MediaStream API录制音频

原文链接:www.sitepoint.com/mediastream…

Media Capture and Streams API 允许你从用户的设备录制音频,然后获取音频轨。你可以直接播放录制的音频或者上传到服务器。

在本教程中,我们会创建使用一个允许用户使用Media Streams API录制音频并上传服务器保存的网站。用户也可以看到并播放上传的音频。

源代码在github上面。

建立服务器

首先我们使用Node.js和Express创建一个服务器。所以,第一步我们要下载Node.js,如果你电脑上没有的话。

创建目录:

创建一个目录来容纳项目,然后进入目录

mkdir recording-tutorial
cd recording-tutorial
复制代码

初始化项目

使用npm初始化项目:

npm init -y
复制代码

参数-y将会使用默认的值创建package.json文件

安装依赖

接着,我们express和nodemon依赖,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和图片等资源放到这里。

Add a script

最后,在package.json文件中的scripts部分添加一个名为start的命令:

"scripts": {
  "start": "nodemon index.js"
},
复制代码

Start the web server 启动服务器

让我们测试一下,输入以下命令启动服务器:

npm start
复制代码

服务器将会在3000端口启动。你可以在浏览器地址栏输入localhost:3000访问,但是你会收到“Cannot GET /” 的消息,因为我们还没有定义任何路由。

创建录制页面

接着,我们会创建网站主页,用户会通过该页面来录制并预览。

创建public目录,在public目录中创建index.html文件,键入以下内容:

<!DOCTYPE html>
<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>
复制代码

页面将会使用 Bootstrap 5 来布局。但现在为止,页面上只有一个用户用来录制的按钮。

注意我们使用的是麦克风的图像。你可以从Iconscout中下载图片或者你可以使用the GitHub repository上面的图像。

下载图片命名为microphone.png并放到 public/assets/images 目录下面。

添加样式

我们把样式放在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 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
复制代码

我们初始化一些后面需要用到的变量。然后创建一个record函数,作为点击recordButton触发的事件监听函数。

function record() {
  //TODO start recording
}

recordButton.addEventListener('click', record);
复制代码

媒体录制

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

只要用户在网站上给予了对应的权限,该方法就允许我们获取并录制音视频流。getUserMedia允许我们访问本地输入设备。

getUserMedia接收一个MediaStreamConstraints对象作为参数,它包含一组约束条件,指定要录制的媒体类型。这些约束条件可以是带有布尔值的音频或视频,例如{audio:true,video:true}

如果某个值是false,说明我们不会访问对应的设备来录制对应的媒体流。

getUserMedia返回一个promise。如果用户在网页上同意录制,promise将会resolve并接受一个 MediaStream 对象,该对象存放着我们捕获的音视频流。

捕获媒体流

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

record函数内,添加以下代码:

//检测浏览器是否支持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) {
  // 开始录制
  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.mediaDevicesnavigator.mediaDevices.getUserMedia 是否定义,因为有些浏览器例如 Internet Explorer, Chrome on Android, 不支持这些对象.

此外,使用getUserMedia必须是安全站点,这意味着要么使用HTTPS,file://加载的页面,要么来自localhost。否则的话,mediaDevicesgetUserMedia将会是undefined。

开始录制

如果条件判断的结果为false(即mediaDevicesgetUserMedia 都被支持),我们首先要把录制按钮的图片改成stop.png,你可以从Iconscout 或者 the GitHub repository 下载图片并放到public/assets/images目录.

然后,我们检查mediaRecorder是否是null,mediaRecorder是我们在文件开头定义的变量。

如果为null,说明没有正在进行的录制。然后我们通过getUserMedia来获取MediaStream的实例来开始录制。

传递{audio:true}作为参数,因为我们只需要录制音频。

浏览器会在这里提示用户是否允许使用麦克风,如果用户允许,getUserMedia返回的promise将会完成(fullfilled),后续代码会依次执行。

mediaRecorder = new MediaRecorder(stream);
mediaRecorder.start();
mediaRecorder.ondataavailable = mediaRecorderDataAvailable;
mediaRecorder.onstop = mediaRecorderStop;
复制代码

以上代码我们创建了MediaRecorder的实例,并赋值给前面创建的mediaRecorder变量。

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

最后,我们为dataavailablestop绑定了事件处理函数,稍后我们会创建对应的函数。

我们也添加了catch的处理逻辑,防止用户不允许使用麦克风或者其他异常抛出的情况。

停止录制

mediaRecorder不为null时以下代码才会执行。如果为null,意味着有一个正在进行录音,而用户正在结束它。所以,我们使用 mediaRecorder.stop() 方法来结束录制。

} else {
  //stop recording
  mediaRecorder.stop();
}
复制代码

处理媒体录制事件

到目前为止,我们的代码实现了用户点击录制和暂停录制。接下来。我们要为dataavailablestop添加事件处理。

On data available

dataavailable在两种情况下会触发,一种情况是录制完成时,另一种情况是调用 mediaRecorder.start()开始录制时传递了timeslice参数,他会指示该事件多久会触发一次。传递timeslice允许对录音进行分片并分块获取数据。

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

function mediaRecorderDataAvailable(e) {
  chunks.push(e.data);
}
复制代码

该chunk将是一个用户录音的音轨阵列。

On stop

在创建处理停止事件的mediaRecorderStop之前,让我们首先添加HTML元素容器,它将容纳录制的音频和按钮SaveDiscard

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元素的一个结点实例。

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 = [];
}
复制代码

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

我们需要做的最后一件事是在index.html中引入record.js。在 "body "的末尾添加 "script"。

<script src="/js/record.js"></script>
复制代码

测试录制

现在让我们测试下。在你的浏览器中进入localhost:3000并点击记录按钮。你会被要求允许该网站使用麦克风。

确保你在localhost或HTTPS服务器上加载网站,即使你使用的是支持的浏览器。否则,MediaDevicesgetUserMedia将不可用。

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

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

接下来,我们将实现SaveDiscard按钮的点击事件。Save按钮应该将音频上传到服务器,而Discard按钮应该将其删除。

Discard 点击事件处理

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

将容纳Discard按钮的变量添加到以下代码的开头

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

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);
复制代码

你现在可以尝试录制一些东西,然后点击Discard按钮。音频播放器将被删除,按钮也被隐藏。

上传到服务器

Save事件处理

现在,我们将实现Save按钮的点击处理程序。当用户点击Save按钮时,这个处理程序将使用Fetch APIaudioBlob上传到服务器。

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

我们在项目根目录创建一个uploads目录。

mkdir uploads
复制代码

然后,在record.js的开头,添加一个变量,用来存放"Save"按钮元素。

const saveAudioButton = document.getElementById('saveButton');
复制代码

然后,在文件中最后,添加下列代码:

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 recording
    resetRecording();
  })
}

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

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

创建API

我们现在需要实现对应的接口,把音频上传到uploads目录中。

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

我们使用npm来安装它。

npm i multer
复制代码

接着,在index.js,添加以下代码在文件的开头

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, `${Date.now()}.${fileNameArr[fileNameArr.length - 1]}`);
  },
});
const upload = multer({ storage });
复制代码

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

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

接下来,我们想让uploads目录下的文件可以公开访问。因此,在app.listen前添加以下内容。

app.use(express.static('uploads'));
复制代码

最后,我们将创建上传的处理逻辑,要做的只是使用upload中间件来上传音频并返回一个JSON响应。

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

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

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

测试上传

让我们来测试一下。再次进入浏览器中的localhost:3000,录制一些东西,然后点击Save按钮。

请求将被发送到后端,文件将被上传,并将向用户显示一个通知,通知他们录音已被保存。

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

显示所有录音

创建接口

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

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

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 });
});
复制代码

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

添加录制容器

接下来,我们将添加一个HTML元素,它将是我们要展示的录音的容器。在body的最后,在record.js脚本之前添加以下内容。

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

</div>
复制代码

通过API获取上传的文件

同时在record.js的开头添加变量来存放#recordings元素。

const recordingsContainer = document.getElementById('recordings');
复制代码

然后,我们将添加一个fetchRecordings函数,调用我们之前创建的接口,然后通过createRecordingElement函数,将数据渲染成音频播放器。

我们还会添加一个playRecording事件监听器,用于播放音频按钮的点击事件。

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';
    }
  }
}
复制代码

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

我们还使用了播放暂停图标,它们会显示在每个录音中。你可以从IconscoutGitHub资源库获得这些图标。

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

因此,在record.js的结尾处调用该函数,并在saveRecording的promise处理程序中调用该函数,以取代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网页,如果你之前上传了任何录音,你现在就能看到它们。你也可以尝试上传新的录音,并看到列表被更新。

用户现在可以录制他们的声音,保存或丢弃它们。用户还可以查看所有上传的录音并播放它们。

总结

MediaStream API允许我们为用户增加媒体功能,如录制音频。此外,MediaStream Web API还允许我们录制视频、录制屏幕截图等。按照本教程给出的信息,再加上 MDNSitePoint提供的教程,你也可以在你的网站上添加其他媒体功能。

猜你喜欢

转载自juejin.im/post/7019281558183870500