许多浏览器现在都能访问用户的视频和音频输入。不过,根据浏览器的不同,这一功能可能体现为一种全动态的内置体验,也可能通过授权给用户设备上的其他应用来实现。
从简单做起,循序渐进
最简易的做法是直接要求用户提供预先录制的文件。其实现步骤是:创建一个简单的文件输入元素,然后添加一个表示我们只能接受音频文件的 accept
过滤器,以及一个表示我们希望直接从麦克风获取文件的 capture
属性。
<input type="file" accept="audio/*" capture />
此方法在所有平台上都有效。在桌面平台上,它会提示用户通过文件系统上传文件(忽略 capture
属性)。在 iOS 上的 Safari 中,它会打开麦克风应用以便您录制音频,然后将其传回网页;在 Android 上,它允许用户选择使用哪一个应用来录制音频,录制完毕后将其传回网页。
用户完成录制并返回网站后,您需要以某种方式掌握文件数据。为 input 元素附加一个 onchange
事件,然后读取事件对象的 files
属性,便可快速获得文件访问权。
<input type="file" accept="audio/*" capture id="recorder" />
<audio id="player" controls></audio>
<script>
const recorder = document.getElementById('recorder');
const player = document.getElementById('player');
recorder.addEventListener('change', function (e) {
const file = e.target.files[0];
const url = URL.createObjectURL(file);
// Do something with the audio file.
player.src = url;
});
</script>
</audio>
获得对文件的访问权后,便可随意对其执行任何操作。例如,您可以:
- 将其直接附加到一个
<audio>
元素,这样便能播放文件 - 将其下载至用户的设备
- 通过将其附加到
XMLHttpRequest
将其上传到服务器 - 通过 Web Audio API 传递文件并对其应用过滤器
尽管使用 input 元素方法获得对音频数据访问权的情况普遍存在,却是最没有吸引力的方案。因为我们真正需要的是获得对麦克风的访问权,直接在页面内提供良好的体验。
以交互方式访问麦克风
现代浏览器可直连麦克风,我们可以借此打造与网页完全集成的体验,让用户永远都不需要离开浏览器。
获取对麦克风的访问权限
我们可以利用 WebRTC 规范中名为 getUserMedia()
的 API 直接访问麦克风。getUserMedia()
会提示用户授予对其相连麦克风和摄像头的访问权。
如果授权成功,该 API 将返回一个 Stream
,其中包含来自摄像头或麦克风的数据,然后我们可以将数据附加到一个 <audio>
元素、将其附加到一个 WebRTC 数据流、将其附加到一个 Web Audio AudioContext
,或使用 MediaRecorder
API 对其进行保存。
如需从麦克风获取数据,我们只需在传递给 getUserMedia()
API 的约束对象中设置 audio: true
。
<audio id="player" controls></audio>
<script>
const player = document.getElementById('player');
const handleSuccess = function (stream) {
if (window.URL) {
player.srcObject = stream;
} else {
player.src = stream;
}
};
navigator.mediaDevices
.getUserMedia({audio: true, video: false})
.then(handleSuccess);
</script>
如果您想选择特定麦克风,可以先枚举可用的麦克风。
navigator.mediaDevices.enumerateDevices().then((devices) => {
devices = devices.filter((d) => d.kind === 'audioinput');
});
然后,您可以在调用 getUserMedia
时传递要使用的 deviceId
。
navigator.mediaDevices.getUserMedia({
audio: {
deviceId: devices[0].deviceId,
},
});
这段代码本身的用处并不大。我们所能做的就是获取音频数据并进行播放。
访问麦克风的原始数据
如需访问麦克风的原始数据,我们需要获取 getUserMedia()
创建的串流,然后使用 Web Audio API 处理数据。Web Audio API 是一个简单的 API,用于获取输入源并将这些输入源连接到可以处理音频数据(调节增益等)的节点,最终目的是连接到扬声器以便用户能够听到声音。
可以连接的其中一个节点是 AudioWorkletNode
。借助此节点,您可以使用低级功能进行自定义音频处理。实际的音频处理是在 AudioWorkletProcessor
中的 process()
回调方法中进行的。调用此函数可提交输入和参数并提取输出。
如需了解详情,请参阅输入音频 Worklet。
<script>
const handleSuccess = async function(stream) {
const context = new AudioContext();
const source = context.createMediaStreamSource(stream);
await context.audioWorklet.addModule("processor.js");
const worklet = new AudioWorkletNode(context, "worklet-processor");
source.connect(worklet);
worklet.connect(context.destination);
};
navigator.mediaDevices.getUserMedia({ audio: true, video: false })
.then(handleSuccess);
</script>
// processor.js
class WorkletProcessor extends AudioWorkletProcessor {
process(inputs, outputs, parameters) {
// Do something with the data, e.g. convert it to WAV
console.log(inputs);
return true;
}
}
registerProcessor("worklet-processor", WorkletProcessor);
保留在缓冲区内的数据是来自麦克风的原始数据,在这些数据的处理上有以下这几种选择:
- 将其直接上传到服务器
- 将其存储在本地
- 将其转换为专用文件格式(例如 WAV),然后保存至服务器或本地
保存来自麦克风的数据
要想保存来自麦克风的数据,最简便的方法是使用 MediaRecorder
API。
MediaRecorder
API 将获取 getUserMedia
创建的卡片信息流,然后渐进式地将卡片信息流中的数据保存到首选目的地。
<a id="download">Download</a>
<button id="stop">Stop</button>
<script>
const downloadLink = document.getElementById('download');
const stopButton = document.getElementById('stop');
const handleSuccess = function(stream) {
const options = {mimeType: 'audio/webm'};
const recordedChunks = [];
const mediaRecorder = new MediaRecorder(stream, options);
mediaRecorder.addEventListener('dataavailable', function(e) {
if (e.data.size > 0) recordedChunks.push(e.data);
});
mediaRecorder.addEventListener('stop', function() {
downloadLink.href = URL.createObjectURL(new Blob(recordedChunks));
downloadLink.download = 'acetest.wav';
});
stopButton.addEventListener('click', function() {
mediaRecorder.stop();
});
mediaRecorder.start();
};
navigator.mediaDevices.getUserMedia({ audio: true, video: false })
.then(handleSuccess);
</script>
在我们这种情况下,我们要将数据直接保存到一个数组中,然后在稍后转换成 Blob
,然后再将其保存到网络服务器,或直接保存在用户设备的存储内。
以负责任的方式请求麦克风使用权限
如果用户之前未授予您的网站对麦克风的访问权限,则在您调用 getUserMedia
时,浏览器会立即提示用户授予您的网站对麦克风的访问权限。
用户讨厌在其机器上收到索要功能强大设备访问权的提示,他们常常会屏蔽权限请求,而如果他们不了解提示的产生环境,也会将其忽略。最佳做法是在首次需要权限时只请求访问麦克风。一旦用户授予了访问权限,就不会再次收到提示,但如果他们拒绝授权,您就无法再次向用户请求权限。
使用 Permissions API 确认是否已获得访问权限
getUserMedia
API 并不能让您了解自己是否已获得对麦克风的访问权。这就带来了一个问题:为了提供友善的界面,让用户愿意授予对麦克风的访问权,您就必须请求获得对麦克风的访问权。
在某些浏览器中,可以利用 Permission API 来解决这个问题。借助 navigator.permission
API,您无需再次提示用户便可查询到访问特定 API 能力的状态。
如需查询是否有权访问用户的麦克风,您可以将 {name: 'microphone'}
传入 query 方法,后者将返回:
granted
- 用户之前已授予对麦克风的访问权;prompt
- 用户尚未授予访问权限,调用getUserMedia
时将会收到提示;denied
- 系统或用户已明确屏蔽对麦克风的访问权限,您将无法获得对其的访问权限。
现在,您可以快速检查,以确认是否需要改动用户界面来适应用户需要执行的操作。
navigator.permissions.query({name: 'microphone'}).then(function (result) {
if (result.state == 'granted') {
} else if (result.state == 'prompt') {
} else if (result.state == 'denied') {
}
result.onchange = function () {};
});