3、媒体流基础:MediaStream API介绍、getUserMedia()方法详解、获取摄像头视频流
好,咱们今天来聊聊媒体流的基础。说白了,WebRTC 的一切音视频通信,起点都是 MediaStream。你想想看,没有流,你拿什么去传?
我个人习惯把 MediaStream 理解成一个“管道”,管道里流淌的是视频轨道和音频轨道。这个管道本身不复杂,复杂的是怎么把摄像头、麦克风的数据灌进去。而 getUserMedia(),就是那个打开水龙头的动作。
核心一句话:MediaStream 是容器,MediaStreamTrack 是内容。getUserMedia() 是获取内容的入口。
3.1 MediaStream API 概览
MediaStream 其实就是一个接口,代表同步的媒体数据流。我在项目中遇到过不少新手,以为 MediaStream 就是视频本身,其实不是。它更像是一个“遥控器”,让你可以控制轨道的开启、关闭,甚至动态增减轨道。
它的主要属性和方法,我列个表给你看,这样更直观:
| 属性/方法 | 说明 | 我用过的场景 |
|---|---|---|
id |
流的唯一标识符(只读) | 调试时区分多个流 |
active |
流是否处于活跃状态(只读) | 判断摄像头是否被占用 |
getVideoTracks() |
获取所有视频轨道 | 拿到轨道后设置参数 |
getAudioTracks() |
获取所有音频轨道 | 静音/取消静音操作 |
addTrack() |
向流中添加轨道 | 动态共享屏幕时加视频轨 |
removeTrack() |
从流中移除轨道 | 关闭某个摄像头画面 |
clone() |
克隆当前流 | 很少用,但多路输出时有用 |
嗯,这里要注意:active 属性很关键。我曾经调试一个 bug,发现流明明拿到了,但画面就是黑的。后来一查,是 active 为 false,因为摄像头被另一个进程占用了。所以,拿到流之后,先检查一下 stream.active,能省不少排查时间。
3.2 getUserMedia() 方法详解
getUserMedia() 是 WebRTC 里最常用的 API 之一。它的签名很简单:
navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
// 成功回调
})
.catch(function(err) {
// 失败回调
});
但简单归简单,坑可不少。我一个个说。
3.2.1 constraints 参数
constraints 是一个对象,用来指定你要获取什么类型的媒体。最基本的写法:
// 只获取视频
const constraints = {
video: true,
audio: false
};
// 只获取音频
const constraints = {
video: false,
audio: true
};
// 都获取
const constraints = {
video: true,
audio: true
};
但实际项目中,你很少会只写一个 true。为什么?因为你需要控制分辨率、帧率、甚至摄像头方向。比如:
const constraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 }
},
audio: {
echoCancellation: true,
noiseSuppression: true
}
};
这里有个细节:ideal 表示理想值,浏览器会尽量满足,但不保证。如果你非要某个值不可,用 exact:
const constraints = {
video: {
width: { exact: 1920 },
height: { exact: 1080 }
}
};
警告:使用 exact 要小心。如果设备不支持该分辨率,getUserMedia() 会直接抛错。我曾经在某个老旧笔记本上测试,指定 4K 分辨率,结果直接挂了。建议先用 ideal,或者通过 enumerateDevices() 先查一下设备能力。
3.2.2 返回值
getUserMedia() 返回的是一个 Promise。成功时 resolve 一个 MediaStream 对象,失败时 reject 一个错误。
常见的错误类型:
- NotAllowedError:用户拒绝了权限。这个最常见,尤其是第一次访问时浏览器会弹窗询问。
- NotFoundError:没有找到指定的设备。比如你要求摄像头,但设备没有。
- NotReadableError:设备被占用。比如另一个应用正在使用摄像头。
- OverconstrainedError:约束条件无法满足。比如你要求 4K 但设备不支持。
- AbortError:未知错误,通常很少见。
小技巧:我习惯在 catch 里根据错误类型给用户不同的提示。比如 NotAllowedError 就引导用户去浏览器设置里开启权限,NotFoundError 就提示检查设备连接。别只弹一个「获取失败」,用户体验太差了。
3.3 获取摄像头视频流实战
理论说完了,咱们直接上代码。下面是一个完整的示例,从获取流到显示在页面上:
<!DOCTYPE html>
<html>
<body>
<video id="localVideo" autoplay playsinline></video>
<button id="startBtn">开启摄像头</button>
<button id="stopBtn">关闭摄像头</button>
<script>
let localStream = null;
document.getElementById('startBtn').onclick = async () => {
try {
const constraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 }
},
audio: false
};
localStream = await navigator.mediaDevices.getUserMedia(constraints);
const video = document.getElementById('localVideo');
video.srcObject = localStream;
// 检查流是否活跃
if (!localStream.active) {
console.warn('流未激活,可能被占用');
}
// 打印轨道信息
const tracks = localStream.getVideoTracks();
console.log(`获取到 ${tracks.length} 个视频轨道`);
tracks.forEach(track => {
console.log(`轨道: ${track.label}, 状态: ${track.readyState}`);
});
} catch (err) {
console.error('获取摄像头失败:', err);
if (err.name === 'NotAllowedError') {
alert('请允许摄像头权限');
} else if (err.name === 'NotFoundError') {
alert('未检测到摄像头设备');
} else {
alert('获取失败: ' + err.message);
}
}
};
document.getElementById('stopBtn').onclick = () => {
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
document.getElementById('localVideo').srcObject = null;
localStream = null;
console.log('摄像头已关闭');
}
};
</script>
</body>
</html>
这段代码我解释几个关键点:
- autoplay 和 playsinline:视频标签必须加这两个属性,否则在移动端 Safari 上不会自动播放。
- srcObject:直接把 MediaStream 赋值给 video 标签,而不是用
src属性。这是标准做法。 - track.stop():关闭摄像头时,要停止所有轨道,并释放资源。否则摄像头灯会一直亮着。
避坑指南:我曾经在关闭摄像头时只调了 stream.stop(),但发现旧版浏览器不支持这个方法。后来统一改用 track.stop(),兼容性更好。另外,记得把 srcObject 置为 null,否则 video 标签还会显示最后一帧画面。
3.4 知识体系结构图
下面我用一张 SVG 图来梳理本章的核心逻辑,方便你理解整个流程:
这张图把整个流程串起来了。从用户操作开始,到调用 API,再到成功或失败的处理。你写代码的时候,脑子里可以过一遍这个流程,不容易漏掉分支。
3.5 关于轨道的一些补充
拿到 MediaStream 之后,你可能会想对轨道做更多操作。比如:
- 启用/禁用轨道:
track.enabled = false可以“静音”视频或音频,但不会释放设备。适合做临时开关。 - 获取轨道设置:
track.getSettings()返回当前轨道的实际参数,比如实际分辨率、帧率。这个在调试时很有用。 - 监听轨道结束:
track.onended事件,当设备被拔出或权限被撤销时触发。
举个例子,我曾在项目中用 track.getSettings() 来检测用户用的是前置还是后置摄像头:
const track = stream.getVideoTracks()[0];
const settings = track.getSettings();
console.log('当前摄像头:', settings.facingMode);
// 输出可能是 'user'(前置)或 'environment'(后置)
嗯,这个 facingMode 属性在移动端特别实用。你可以根据它来切换 UI 上的翻转按钮图标。
个人经验:我建议你在开发时,把 track.getSettings() 的结果打印到控制台。很多时候你以为摄像头输出的是 720p,实际可能只有 480p。浏览器会根据性能自动降级,别被 ideal 值骗了。
好了,关于 MediaStream 和 getUserMedia 的基础就聊到这里。记住几个关键点:constraints 要合理设置、错误处理要细致、轨道操作要记得释放资源。下一节我们会深入聊聊如何切换摄像头和调整分辨率,到时候这些基础就派上用场了。
公众号:蓝海资料掘金营,微信deep3321