译者:以后再也不翻这么长的文章了,好累……
媒体捕获和流 API(又名流媒体 API)允许你从用户的麦克风录音,然后获取记录的音频或媒体元素作为音轨。你可以在录制后直接播放音轨,或者上传媒体到你的服务器。
在这个教程中,我们将创建一个网站,使用流媒体 API 让用户录制一些东西,然后上传录制音频到服务器保存。用户将可以看到和播放上传的所有录制。
你能在 这个 GitHub 仓库 找到这个教程的完整代码。
设置服务器
我们以创建 Node.js 和 Express 服务端为开始。所以首先确认 下载和安装 Node.js 如果你的机器上还没有这些。
创建目录
创建一个保存此项目的新目录,并进入该目录:
mkdir recording-tutorial
cd 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 端口的服务端,除非端口在环境中被设置过,它暴露 public/assets 目录——我们即将创建——存放 JavaScript、CSS 文件和图片。
添加脚本
最后,在 package.json 的 scripts 下添加一个 start 脚本:
"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">
<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 下载该图标,或者用 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 文件:
// 初始化我们将用的元素
const recordButton = document.getElementById('recordButton');
const recordButtonImage = recordButton.firstElementChild;
let chunks = []; // 将后面被用于录音
let mediaRecorder = null; // 将在后面被用于录音
let audioBlob = null; // 将保存录音的 blob
我们初始化这些我们后面要用到的变量。然后创建一个 record 函数,它是 recordButton 的 click 事件的事件监听器。
function record() {
//TODO 开始录音
}
recordButton.addEventListener('click', record);
我们还要把这个函数作为事件监听器绑定到录音按钮上。
媒体录制
为了开始录制,我们需要使用 mediaDevices.getUserMedia() 方法。
这个方法允许我们获取流、录制用户的音频和/或视频,仅当用户提供让网站那样做的权限。getUserMedia 方法允许我们访问本地输入设备。
getUserMedia 接收 MediaStreamConstraints 对象作为参数,该对象包含了一组限制,该限制明确我们从 getUserMeida 获得的哪种媒体类型是想要的。
如果值为 false,意味着我们不会访问设备或者录制媒体。
getUserMedia 返回一个 Promise。如果用户允许网站录制,该 promise 的成功回调会接收到一个 MediaStream 对象,我们能用它捕获用户的视频或音频流。
媒体捕获和流
为了使用流媒体 API 对象捕获媒体,我们需要使用 MediaRecorder 接口。我们将要创建一个该接口的新对象,它的构造函数接收 MediaStream 对象、从而使我们能容易的通过它的方法控制录制。
在 record 函数中,添加如下代码:
// 检查浏览器是否支持 getUserMedia
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
alert('Your browser does not support recording!');
return;
}
// 浏览器支持 getUserMedia
// 改变按钮中的图片
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}`);
// 改变按钮中的图片
recordButtonImage.src = '/images/microphone.png';
});
} else {
// 停止录制
mediaRecorder.stop();
}
浏览器支持
我们先检查 navigator.mediaDevices 和 navigator.mediaDevices.getUserMedia 是否定义了,因为有些浏览器像 IE,Chrome 安卓版,或其他的 不支持。
此外,使用 getUserMedia 要求 安全网站,即页面加载使用 HTTPS,file:// 或者来源于localhost。所以,如果页面加载不安全,mediaDevices 和 getUserMedia 会是未定义的。
开始录制
如果条件是 false(换言之,mediaDevices 和 getUserMedia 都支持),我们先将按钮的录制图片改成 stop.png,你能从 Iconscout 和 GitHub 仓库 下载并放到 public/assets/images 里。
然后,我们检查 mediaRecord ——我们在文件开头定义的——是否是 null。
如果它是 null,它表示没有进行中的录制。所以,我们用 getUserMedia 获取一个 MediaStream 实例开始录制。
这里浏览器提示用户允许网站访问麦克风。如果用户允许,成功回调里的代码将被执行:
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.start();
mediaRecorder.ondataavailable = mediaRecorderDataAvailable;
mediaRecorder.onstop = mediaRecorderStop;
这里我们创建新的 MediaRecorder,赋值给我们在文件开头定义的 mediaRecorder。
我们给构造函数传从 getUserMedia 接收的流。然后,我们用 mediaRecorder.start() 开始录制。
最后,我们绑定事件句柄(我们稍候创建)到两个事件上,dataavailable 和 stop。
我们也要添加 catch 句柄万一用户不允许网站访问麦克风或者其他任何 可能抛出的异常。
停止录制
这都发生在 mediaRecorder 不是 null 的情况下。如果它是 null,说明存在进行中的录制并且用户结束它。所以,我们使用 mediaRecorder.stop() 方法停止录制。
} else {
//stop recording
mediaRecorder.stop();
}
处理媒体录制事件
目前我们的代码在用户点击录制按钮时开始和停止录制。接下来,我们将给 dataavailable 和 stop 添加事件句柄。
数据可用时
dataavailable 在全部录制完成时被触发,或者基于被传给 mediaRecorder.start() 的可选参数 timeslice、它表明这个事件被触发的毫秒数。传递 timeslice 允许分片录制以及以小块的形式获取。
创建 mediaRecorderDataAvailable 函数,它将处理 dataavailable 事件,仅以添加收到的 BlobEvent 参数上的 Blob 音频到 chunks 数组里,此数组在文件开头定义过:
function mediaRecorderDataAvailable(e) {
chunks.push(e.data);
}
该小块将是用户录制的音频追踪数组。
停止时
在创建处理 stop 事件的 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 元素的节点实例:
const recordedAudioContainer = document.getElementById('recordedAudioContainer');
我们现在可以实现 mediaRecorderStop。这个函数先移除之前录制或未保存的 audio 元素,创建一个新的 audio 元素,把 src 设置为录制流的 Blob,并显示容器:
function mediaRecorderStop () {
// 检测是否存在之前的录制并移除它们
if (recordedAudioContainer.firstElementChild.tagName === 'AUDIO') {
recordedAudioContainer.firstElementChild.remove();
}
// 创建一个新的保存录音的 audio 元素
const audioElm = document.createElement('audio');
audioElm.setAttribute('controls', ''); // 添加 controls
// 从 chunk 创建 Blob
audioBlob = new Blob(chunks, { type: 'audio/mp3' });
const audioURL = window.URL.createObjectURL(audioBlob);
audioElm.src = audioURL;
// 展示 audio
recordedAudioContainer.insertBefore(audioElm, recordedAudioContainer.firstElementChild);
recordedAudioContainer.classList.add('d-flex');
recordedAudioContainer.classList.remove('d-none');
// 重置为默认
mediaRecorder = null;
chunks = [];
}
在最后,我们重置 mediaRecorder 和 chunks 为初始值以便处理下次录制。用这个代码,我们的网址应该能录音,并当用户停止,能允许他们播放录制的音频。
最后我们要做的事是在 index.html 里连接 record.js。在 body 末尾添加 script:
<script src="/js/record.js"></script>
测试录制
我们现在去看效果。浏览器里访问 localhost:3000 并点击 录制 按钮,你将被询问是否允许网站使用麦克风。
确认你是以 localhost 或 HTTPS 加载网站即使你用的是支持的浏览器。媒体设备和 getUserMedia 在其他情况是不可用的。
点击 允许。麦克风图片将变成停止图片。同时,你应该能看到录音图标在浏览器的地址栏中。这表示麦克风正被网站访问。
尝试录制几秒钟。然后点击 停止 按钮。按钮的图片将变回麦克风图片,并且音频播放器和两个按钮—— 保存 和 丢弃 会出现。
接下来,我们是想 保存 和 丢弃 按钮点击事件。保存 按钮应该上次音频到服务端,丢弃 按钮应该移除它。
丢弃点击事件句柄
我们先实现 丢弃 按钮的事件句柄。点击这个按钮应该先向用户提示确认他们想丢弃录制。然后,如果用户确认了,它将移除音频播放器和隐藏按钮。
在 public/assets/js/record.js 开头添加引用 丢弃 按钮的变量:
const discardAudioButton = document.getElementById('discardButton');
然后,在文件末尾添加下面的内容:
function discardRecording () {
// 提示用户确认他们想丢弃
if (confirm('Are you sure you want to discard the recording?')) {
// 丢弃刚刚录制的音频
resetRecording();
}
}
function resetRecording () {
if (recordedAudioContainer.firstElementChild.tagName === 'AUDIO') {
// 移除 audio
recordedAudioContainer.firstElementChild.remove();
// 因此 recordedAudioContainer
recordedAudioContainer.classList.add('d-none');
recordedAudioContainer.classList.remove('d-flex');
}
// 重置 audioBlob 以便下次录制
audioBlob = null;
}
// 给按钮添加事件监听
discardAudioButton.addEventListener('click', discardRecording);
你现在能试试随便录制,然后点击 丢弃 按钮。音频播放器将被移除并且按钮被隐藏。
上传到服务端
保存点击事件句柄
现在,我们将实现 保存 按钮的点击句柄。这个句柄将在用户点击 保存 按钮时用 Fetch API 上传 audioBlob 到服务端。
如果你对 Fetch API 不熟悉,你可以在我们的“Fetch API 介绍”教程学习更多。
让我们开始在项目根目录创建 uploads:
mkdir uploads
然后,在 record.js 开头,添加一个引用 保存 按钮元素的变量:
const saveAudioButton = document.getElementById('saveButton');
然后,在末尾,添加如下内容:
function saveRecording () {
// 持有上传的 Blob 的表单数据
const formData = new FormData();
// 添加 Blob 到 formData
formData.append('audio', audioBlob, 'recording.mp3');
// 像终端发送请求
fetch('/record', {
method: 'POST',
body: formData
})
.then((response) => response.json())
.then(() => {
alert("Your recording is saved");
// 重置以便下次录制
resetRecording();
// TODO 获取录制
})
.catch((err) => {
console.error(err);
alert("An error occurred, please try again later");
// 重置以便下次录制
resetRecording();
})
}
// 添加事件句柄到点击事件
saveAudioButton.addEventListener('click', saveRecording);
注意,一旦录音上传了,我们使用 resetRecording 重置音频以便下次录制。后面,我们将获取全部录音展示给用户。
创建 API 终端
我们现在需要实现 API 终端,该终端将上传音频到 uploads 目录。
为了在 Express 中方便的处理上传,我们将使用 Multer 库。Multer 通过一个中间件来处理文件上传。
运行下面的命令安装啊它:
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。
请注意,通常,你需要执行 文件验证 并确保正确的,预期的文件类型被上传。简单起见,我们在这个教材中忽略了。
测试上传
让我们测试一下。在浏览器再次访问 localhost:3000,录制一些事,并点击 保存 按钮。
此请求将被发送到终端,文件将被上传,并将向用户展示一个警示框通知他们录音已被保存。
你可以通过检查 项目根目录中的 uploads 目录确认音频事实上已被上传。
展示录音
创建一个 API 终端
我们要做的最后一件事是向用户展示全部录音以便用户能播放它们。
首先,我们将创建用于获取全部文件的终端。在 index.js 的 app.listen 前添加如下内容:
app.get('/recordings', (req, res) => {
let files = fs.readdirSync(path.join(__dirname, 'uploads'));
files = files.filter((file) => {
// 检查文件是音频文件
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) {
// 移除全部之前显示的录制
recordingsContainer.innerHTML = '';
response.files.forEach((file) => {
// 创建录制元素
const recordingElement = createRecordingElement(file);
// 把它加进录制容器
recordingsContainer.appendChild(recordingElement);
})
}
})
.catch((err) => console.error(err));
}
// 创建录制元素
function createRecordingElement (file) {
// 容器元素
const recordingElement = document.createElement('div');
recordingElement.classList.add('col-lg-2', 'col', 'recording', 'mt-3');
// audio 元素
const audio = document.createElement('audio');
audio.src = file;
audio.onended = (e) => {
// 当音频结束,再修改按钮图片为播放状态
e.target.nextElementSibling.firstElementChild.src = 'images/play.png';
};
recordingElement.appendChild(audio);
// 按钮元素
const playButton = document.createElement('button');
playButton.classList.add('play-button', 'btn', 'border', 'shadow-sm', 'text-center', 'd-block', 'mx-auto');
// 按钮图片
const playImage = document.createElement('img');
playImage.src = '/images/play.png';
playImage.classList.add('img-fluid');
playButton.appendChild(playImage);
// 给按钮添加事件监听以便播放录音
playButton.addEventListener('click', playRecording);
recordingElement.appendChild(playButton);
// 返回容器元素
return recordingElement;
}
function playRecording (e) {
let button = e.target;
if (button.tagName === 'IMG') {
// 获取父元素按钮
button = button.parentElement;
}
// 获取 audio 兄弟元素
const audio = button.previousElementSibling;
if (audio && audio.tagName === 'AUDIO') {
if (audio.paused) {
// 如果音频暂停,播放它
audio.play();
// 修改按钮图片为暂停状态
button.firstElementChild.src = 'images/pause.png';
} else {
// 如果音频播放,暂停它
//if audio is playing, pause it
audio.pause();
// 修改按钮图片为播放状态
button.firstElementChild.src = 'images/play.png';
}
}
}
注意,在 playRecording 函数中,我们用 audio.paused 检查音频是否正在播放,它在当时音频未播放时返回 true。
我们还用了在每次录制中出现的 播放 和 暂停 图标,你可以从 Iconscout 或 the 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 网站,如果你之前已上传过一些录音,你现在将能看到它们。你也可以尝试上传新的录音并看到列表更新。
用户现在可以录制他们的声音,保持或丢弃它们。用户还可以看到全部上传的录音并能播放它们。
总结
使用流媒体 API 允许我们未用户添加媒体功能,例如录音。流媒体 Web API 还允许录视频,截屏,等等。跟随这个教程里的信息,以及 MDN 和 SitePoint 提供实用教程,你也能够添加全部其他媒体功能到你的网站上。