前端如何录制音视频
在当今高度互联的数字世界里,声音和影像早已超越静态的文字与图片,成为传递信息、表达情感、连接用户的核心媒介。无论是进行在线会议、录制教学视频、构建社交互动功能,还是实现实时的语音笔记或客服对话,音视频的捕获与录制能力已成为现代 Web 应用不可或缺的一部分。作为前端开发者,我们不再仅仅满足于展示内容,更要赋予用户创造内容的能力——让用户只需点击一个按钮,就能轻松地将自己的声音、影像,甚至屏幕活动永久留存下来
小贴士
文章中涉及到的示例代码你都可以从 这里查看
媒体捕获
本章将深入拆解浏览器媒体捕获的完整技术链:从权限请求的博弈艺术、设备约束的精细调控,到媒体流的动态编排
你将不再止步于“能调用摄像头”,而是真正掌握:
- 高兼容性的设备访问策略
- 专业级的媒体流控制技巧
- 复杂场景下的故障逃生方案
getUserMedia
navigator.mediaDevices
会提示用户给予使用媒体输入的许可,媒体输入会产生一个 MediaStream ,里面包含了请求的媒体类型的轨道。此流可以包含一个视频轨道
(来自硬件或者虚拟视频源,比如相机、视频采集设备和屏幕共享服务等等)、一个音频轨道
(同样来自硬件或虚拟音频源,比如麦克风、A/D 转换器等等),也可能是其他轨道类型
async function getUserMedia() {
try {
const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
console.log(mediaStream);
} catch (error) {
if (error.name === 'NotAllowedError') {
console.log('用户拒绝了访问设备');
}
}
2
3
4
5
6
7
8
9
10
当调用getUserMedia
时会拉起浏览器的授权,用户只有同意授权后才会拿到媒体流
媒体约束
getUserMedia
接受一个constraints
参数,用来定制媒体流的基本要求,参数类型如下:
interface MediaStreamConstraints {
audio?: boolean | MediaTrackConstraints;
video?: boolean | MediaTrackConstraints;
}
interface MediaTrackConstraints {
deviceId?: ConstrainDOMString;
facingMode?: ConstrainDOMString;
height?: ConstrainULong;
width?: ConstrainULong;
// 等等..
}
2
3
4
5
6
7
8
9
10
11
12
constraints 参数是一个包含了video
和 audio
两个成员的 MediaStreamConstraints
对象,用于说明请求的媒体类型,必须至少拥有一个类型或者两个同时可以被指定,默认情况下都会包含
{ audio: true, video: true }
如果为某种媒体类型设置了 true ,得到的结果的流中就需要有此种类型的轨道。如果其中一个由于某种原因无法获得,getUserMedia() 将会产生一个错误
你也可以指定某种类型轨道更细腻的参数,比如想要视频的分辨率为指定的分辨率:
{
audio: true,
video: { width: 1280, height: 720 }
}
2
3
4
浏览器会试着满足这个请求参数,但是如果无法准确满足此请求中参数要求或者用户选择覆盖了请求中的参数时,有可能返回其他的分辨率
强制要求获取特定的尺寸时,可以使用关键字min
、max
或者 exact(就是 min == max)
。以下参数表示要求获取最低为 1280x720
的分辨率
{
audio: true,
video: {
width: { min: 1280 },
height: { min: 720 }
}
}
2
3
4
5
6
7
如果摄像头不支持请求的或者更高的分辨率,返回的 Promise 会处于 rejected 状态,NotFoundError
作为rejected 回调的参数,而且用户将不会得到要求授权的提示
当请求包含一个 ideal
(应用最理想的)值时,这个值有着更高的权重,意味着浏览器会先尝试找到最接近指定的理想值的设定或者摄像头(如果设备拥有不止一个摄像头)
{
audio: true,
video: {
width: { ideal: 1280 },
height: { ideal: 720 }
}
}
2
3
4
5
6
7
比如你还可以指定摄像头为后置摄像头:
{ audio: true, video: { facingMode: "user" } }
媒体流的处理与控制
媒体流是一个 MediaStream
接口类型的媒体内容的流。一个流可能包含很多轨道,但通常只有视频和音频2种类型的轨道
const videoTracks = stream.getVideoTracks(); // 获取所有视频轨道
const audioTracks = stream.getAudioTracks(); // 获取所有音频轨道
const allTracks = stream.getTracks(); // 获取所有轨道
2
3
每个轨道都是 MediaStreamTrack 类型的接口对象,track类型中有多个有用的属性和方法,其中以下表格中的信息都比较重要,读者可以翻阅文档加深印象:
操作 | 代码示例 | 应用场景 |
---|---|---|
禁用轨道(暂停) | track.enabled = false; | 视频通话中临时关闭摄像头 |
启用轨道(恢复) | track.enabled = true; | 重新开启麦克风 |
永久停止轨道 | track.stop(); | 结束录制释放设备资源 |
获取约束 | track.getConstraints() | 查看媒体源约束 |
应用新的约束 | track.applyConstraints() | 根据网络质量调整约束 |
获取设置信息 | track.getSettings() | 查看一些信息 |
比如上面的enabled
属性,当调整它的值为false
时,MediaStream
中将不会收取到该轨道的有用数据,例如视频中临时关闭视频、麦克风等等
// 假设这个是 摄像头 媒体轨道
const videoTrack = sream.getVideoTracks()[0];
// 关闭摄像头
videoTrack.enabled = false;
2
3
4
5
而stop
方法与enabled
不同的是会直接销毁掉当前媒体轨道,并且与MediaStream失去连接,这样就不会收到该媒体轨道资源了,除非重新开启当前轨道并加入MediaStream
;相反enabled
不会与MediaStream
断开连接,而是该轨道的目标设备源本身不会产生任何数据
🎉🎉🎉
媒体流对象MediaStream
也有很多重要的属性和方法,下面表格中都是比较重要的:
操作 | 代码示例 | 应用场景 |
---|---|---|
获取指定媒体类型轨道 / 获取所有媒体轨道 | stream.getAllTracks() 、stream.getAudioTracks() | 动态操作某个媒体轨道 |
添加新的媒体轨道 | stream.addTrack() | 切换媒体设备时,将新的媒体加入 |
删除媒体轨道 | stream.removeTrack() | 切换媒体设备或者关闭时,删除媒体轨道 |
除此之外 MediaStream
构造器也允许接收多个媒体源,这样在混合不同的媒体资源时就很有用:
// 将某个视频流和多个音频流混入一起
const mixedStream = new MediaStream([
videoStream.getVideoTracks()[0],
...audioStream1.getAudioTracks(),
...audioStream2.getAudioTracks(),
])
2
3
4
5
6
屏幕捕获
借助浏览器提供媒体设备功能,允许JS可以获取屏幕设备媒体流,类似于录屏软件选择不同的区域或者App录屏,当然浏览器下的功能并没有那么强大
通过 MediaDevices.getDisplayMedia
可以调起屏幕录取授权,与获取用户设备流程一致,只有用户授权了才可以获取媒体资源
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: "always", // 显示鼠标指针
},
audio: true // 捕获系统音频(需浏览器支持)
});
2
3
4
5
6
getDisplayMedia
的参数与上面getUserMedia
参数一致,细节可查看源码,这里就不多说了
当用户确认授权时,就会获得对应屏幕媒体流。目前getDisplayMedia
允许获取浏览器Tab
、指定App
、指定屏幕
3种类型的目标屏幕
设备列表
除了捕获默认的视频或音频设备媒体流外,还可以查询当前设备所有支持的视频、音频设备
MediaDevices
的方法 enumerateDevices 请求一个可用的媒体输入和输出设备的列表,例如麦克风,摄像机,耳机设备等。此操作是一个异步行为,会返回描述设备的 MediaDeviceInfo 的数组
通常设备分为输入设备
和输出设备
,输入设备为MediaDeviceInfo
子类 InputDeviceInfo 类型
const devices = await navigator.mediaDevices.enumerateDevices();
console.log(devices);
2
运行这段代码后,返回当前设备中所有音视频设备列表,下面是本机器的设备列表:
📢:由于授权或者连接问题,可能获取一次设备列表后,再次获取设备列表可能会增加,读者可以亲自试试
MediaDeviceInfo对象由以下几个重要属性组成:
- deviceId:设备的唯一ID标识,在指定设备时很关键
- kind:设备类型
audioinput
、audiooutput
、videoinput
、videooutput
- label:设备的名称,或指定的设备名称,在显示设备列表时很有用
- groupId:当设备来自同一物理设备时,groupId一致
那么这里获取设备列表有什么用处呢❓这里就可以用上前面所讲到的媒体流捕获了,在捕获媒体流时是可以指定设备id的
假如有3个麦克风,而用户现在就想使用AirPods
,那么就可以指定deviceId
为AirPods
:
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: {
deviceId: 'xxxx' // 假设这里是目标设备的id
}
})
2
3
4
5
6
这里就不做演示了,读者感兴趣可以尝试一下
媒体流实时渲染
知道怎么捕获包含视频或音频的媒体流了,那么如何查看当前捕获的媒体流是什么样呢?比如当前摄像头捕获的画面是啥样的❓
回显实时画面的方法有很多,最常用的为以下几种:
- video:通过
video.srcObject
可以快速显示媒体流画面 - canvas:通常用来显示自定义画面、或者需要一些处理的画面
- WebGL:适用于图形、视觉计算
下面就结合上面所讲的知识,通过录制 网易云音乐App
然后实时渲染画面:
mediaStream = await navigator.mediaDevices.getDisplayMedia({
video: { cursor: "always", }
});
const video = document.createElement('video');
video.srcObject = mediaStream;
video.play();
document.body.appendChild(video);
2
3
4
5
6
7
8
9
如果不出意外将会正常显示屏幕画面
如果需要对画面进行加工处理,如:同时渲染多个画面(多个视频媒体源轨道)、添加贴上等等,单一的 video.srcObject
就无法实现了,那么canvas来就派上用场了,具体为canvas
的 drawImage 能力,读者继续往下阅读,后面有对应的案例讲解
画质调整
上网高峰期难免会有网络波动,如果画质太高就会出现延时、卡顿,那么就需要同步降低画质,这样网络负担就小一点
上面介绍了媒体流和轨道的控制与处理,根据applyConstraints
结合connection
可以快速的实现这种效果:
// 获取网络连接信息
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (connection) {
connection.addEventListener('change', () => {
handleNetworkChange(connection.effectiveType);
});
}
function handleNetworkChange(networkType) {
console.log('网络类型:', networkType)
if (!mediaStream) return
const videoTrack = mediaStream.getVideoTracks()[0];
const constraints = videoTrack.getConstraints();
if (networkType === 'slow-2g' || networkType === '2g' || networkType === '3g') {
console.warn('网络质量差,启用低分辨率模式');
videoTrack.applyConstraints({
...constraints,
width: 320,
frameRate: 15
});
// 调整视频质量/减少数据传输等
} else if (networkType === '4g') {
console.log('网络良好,启用高清模式');
videoTrack.applyConstraints({
...constraints,
width: 1280,
height: 720,
frameRate: 30
});
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
看效果,切换不同的网络时,对应的画面质量
媒体录制
恭喜你🎉,到这里关于本地的媒体流捕获(Remote MediaStream:WebRTC..)、显示方面的知识就掌握差不多了,现在我们进入下一个话题
如果用户想要在捕获媒体流时,也想实时录制,最后回放整体过程该怎么做❓
别慌,MediaRecorder 就是干这个的,此方法构造器接受2个参数:媒体流
、配置参数
new MediaRecorder(stream)
new MediaRecorder(stream, options)
2
配置选项可以指定录制后的媒体的mimeType
、相关比特率等等。其返回的实例有以下几个重要的事件、方法:
方法/事件 | 作用 |
---|---|
start | 开始录制 |
stop | 停止录制 |
pause | 暂停录制 |
resume | 恢复录制 |
ondataavailable | 每次周期性录制媒体流资源事件 |
onstop | 当停止录制时触发事件,其它事件类推 |
基础录制
基础录制直接借助video
可实现单一视频媒体流的录制,非常简单
这里以上面捕获 网易云音乐App
为例,在进行捕获时也进行录制,最后播放录制好的视频:
<button id="start">开始</button>
<script>
const startBtn = document.getElementById('start');
let recordBtn;
let videoElem;
let mediaStream;
let recorder;
let isRecording = false;
const blobChunks = [];
startBtn.addEventListener('click', start);
async function start() {
startBtn.remove();
mediaStream = await navigator.mediaDevices.getDisplayMedia({
video: { cursor: "always", }
});
videoElem = document.createElement('video');
videoElem.srcObject = mediaStream;
videoElem.play();
document.body.appendChild(videoElem);
recordBtn = document.createElement('button');
recordBtn.innerText = '开始录制';
document.body.appendChild(recordBtn);
// 点击开始录制
recordBtn.addEventListener('click', startRecording);
}
function startRecording() {
if (isRecording) {
recorder.stop();
isRecording = false;
return;
}
if (!isRecording) {
recordBtn.innerText = '正在录制,点击停止';
isRecording = true;
}
recorder = new MediaRecorder(mediaStream, {
mimeType: 'video/webm; codecs=vp9'
});
recorder.ondataavailable = function (e) {
blobChunks.push(e.data);
};
recorder.onstop = stopRecording;
recorder.start(30);
}
function stopRecording() {
const blob = new Blob(blobChunks, { type: "video/webm;codecs=h264" });
blobChunks.length = 0;
const link = URL.createObjectURL(blob);
window.open(link, "_blank");
recorder = null;
videoElem && videoElem.remove();
recordBtn && recordBtn.remove();
blobChunks.length = 0;
mediaStream && mediaStream.getTracks().forEach(track => track.stop());
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
上面代码运行后,在本机器上的效果如下
需要注意的是此API的兼容性目前仍有一些问题,比如录制的媒体流mimeType
非常有限制,大部分都只支持webm
形式的,读者可在PC、Phone设备上不同的浏览器进行尝试
复杂场景录制
相比一个视频源只需要srcObject
就可以搞定,当有多个视频源时就需要将多个视频媒体流混合显示了,单纯靠srcObject
已经无法满足了,这时候就需要借助canvas
来自绘制画面了。常见的场景如:在线教育老师视频画面和画板或屏幕同时出现、在线会议等等
读者应该都知道canvas的drawImage
不仅可以绘制图像,也可以捕获video
帧图像然后绘制,那么我们就可以控制如何绘制画面了,如:将屏幕作为背景绘制、用户视频作为一小部分窗口绘制在某个区域
多个视频画面都显示在canvas画布上了,那么如何进行录制呢❓canvas提供了 CaptureStream 方式:
canvas.captureStream(frameRate?): MediaStream
此方法会实时录制canvas画布内容并返回媒体流对象,那么一切都可以对上了,直接将其丢给MediaRecorder
不就可以了
下面是一个小案例演示视频,还是以 网易云音乐App
案例为基础,将其作为背景,然后用户视频画面裁剪成圆形,放置在画布右下角:
该过程中也演示了,人像摄像头画面的开/关
效果,当然这里没有将关闭后的人像画面移除掉,此处仅仅演示效果
由于篇幅原因这里不再列出具体的代码了,读者可以通过文章开头的案例源码链接获取
下期预告
文章到这里就接近尾声了,通过以上文章内容,读者基本上可以掌握录制音频、视频等相关技能了
下期我们将会介绍音频可视化
知识,音频可视化应用场景非常广泛,如:录音时音轨展示、播放器音效,如果你使用网易云音乐应该看到过这种效果:
归根都是 AudioContext 和 AnalyserNode 的能力;当然音视频少不了端到端通话,WebRTC 赋予Web应用程序和站点能够捕获并选择流式传输音频和/或视频媒体,并且可以在浏览器之间交换任意数据而无需中介,实时通话我们后面再讲