3、媒体流基础:MediaStream与MediaStreamTrack详解、getUserMedia实战、屏幕共享
好,咱们今天聊点实在的。媒体流,说白了就是WebRTC世界里最基础的“水管”。你想想看,没有水,你修再漂亮的管道也没用。在WebRTC里,这个“水”就是音频和视频数据,而水管就是MediaStream和MediaStreamTrack。
我个人习惯把MediaStream想象成一个“轨道容器”。它本身不产生数据,但它负责把多个轨道(Track)组织在一起,统一管理。比如一个视频通话,通常包含一个音频Track和一个视频Track,它们被装进同一个MediaStream里,然后通过网络发送给对方。
MediaStream与MediaStreamTrack:谁是谁?
先搞清楚这两个概念,别搞混了。
- MediaStream:一个容器,可以包含多个MediaStreamTrack。它有一个id属性,用来唯一标识这个流。
- MediaStreamTrack:真正的数据源。它代表一个单独的媒体轨道,比如摄像头的一路视频,或者麦克风的一路音频。
嗯,这里要注意:一个MediaStream可以包含多个相同类型的Track吗?理论上可以,但实际项目中很少这么干。我见过有人试图在一个流里塞两个视频Track,结果接收端处理起来非常麻烦。我的建议是:一个流里只放一个视频Track和一个音频Track,简单清晰。
核心关系图
getUserMedia实战:拿到你的第一滴水
getUserMedia是浏览器提供的一个API,用来获取用户的摄像头和麦克风权限。说白了,就是问用户:“嘿,我能用你的摄像头吗?”
最基本的用法是这样的:
// 请求视频和音频
async function startCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// 把视频流绑定到video元素上
const videoElement = document.getElementById('localVideo');
videoElement.srcObject = stream;
} catch (err) {
console.error('获取媒体流失败:', err);
}
}
这段代码看起来简单,但坑不少。我曾经在一个项目中,用户反馈说摄像头打不开,排查了半天才发现是用户把摄像头权限给禁了。所以,一定要处理错误情况。
避坑指南
我曾经遇到过一个诡异的问题:在iOS Safari上,getUserMedia返回的stream是正常的,但视频就是黑屏。后来发现是因为没有在用户手势(比如点击按钮)的回调里调用getUserMedia。iOS Safari要求getUserMedia必须在用户交互事件中触发,否则会静默失败。嗯,这个坑我踩过,你们别踩了。
更精细的控制:约束条件
你不想随便拿个分辨率就完事吧?getUserMedia支持传入约束条件,让你指定想要的摄像头参数。
const constraints = {
video: {
width: { min: 640, ideal: 1280, max: 1920 },
height: { min: 480, ideal: 720, max: 1080 },
frameRate: { ideal: 30, max: 60 },
facingMode: 'user' // 前置摄像头
},
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 48000
}
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
这里有个小技巧:ideal表示理想值,浏览器会尽量满足,但如果不满足,它会自动降级到最接近的值。min和max是硬性要求,如果浏览器无法满足,会直接报错。我个人习惯只设ideal,不设min和max,这样兼容性最好。
屏幕共享:getDisplayMedia
屏幕共享和摄像头采集不一样。摄像头采集用的是getUserMedia,屏幕共享用的是getDisplayMedia。为什么分开?因为屏幕共享涉及隐私,浏览器会弹出一个更严格的权限对话框,让用户选择共享整个屏幕、某个窗口还是某个浏览器标签页。
async function startScreenShare() {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
cursor: 'always' // 显示鼠标指针
},
audio: false // 屏幕共享一般不采集音频
});
// 把屏幕共享流绑定到video元素
const screenVideo = document.getElementById('screenVideo');
screenVideo.srcObject = stream;
} catch (err) {
console.error('屏幕共享失败:', err);
}
}
注意,getDisplayMedia返回的stream里,video track的kind仍然是"video",但它的label会变成类似"Entire Screen"或"Window Title"这样的描述。你可以通过track.label来判断用户共享的是什么。
个人经验
我在做远程协作工具时,发现屏幕共享的帧率不能设太高。设30fps的话,CPU占用会飙升,尤其是共享整个4K屏幕时。我建议屏幕共享用10-15fps就够了,用户看起来不会觉得卡,而且性能开销小很多。
Track的生命周期管理
拿到Track之后,你得知道怎么控制它。每个MediaStreamTrack都有几个关键方法:
| 方法/属性 | 说明 | 使用场景 |
|---|---|---|
| track.enabled | 启用或禁用轨道 | 静音/取消静音 |
| track.stop() | 停止轨道,释放资源 | 关闭摄像头 |
| track.readyState | 返回"live"或"ended" | 检查轨道是否还在运行 |
| track.clone() | 克隆一个相同的轨道 | 需要同时发送给多个Peer |
举个例子,你想实现一个“静音”按钮:
function toggleMute(stream) {
const audioTrack = stream.getAudioTracks()[0];
if (audioTrack) {
audioTrack.enabled = !audioTrack.enabled;
// 更新UI
document.getElementById('muteBtn').textContent =
audioTrack.enabled ? '静音' : '取消静音';
}
}
这里有个细节:enabled设为false时,轨道仍然存在,只是不输出数据。这比直接stop()要好,因为stop()之后轨道就彻底结束了,无法恢复。
从Stream中提取Track
有时候你需要单独操作某个Track。比如,你想把视频Track替换成屏幕共享的Track,但保留音频Track不变。
// 获取当前流的视频Track
const oldVideoTrack = localStream.getVideoTracks()[0];
// 获取屏幕共享流的视频Track
const screenTrack = screenStream.getVideoTracks()[0];
// 替换轨道
localStream.removeTrack(oldVideoTrack);
oldVideoTrack.stop(); // 关闭摄像头
localStream.addTrack(screenTrack);
// 然后通过RTCPeerConnection发送更新后的流
// 注意:需要重新negotiate
嗯,这里要注意:替换Track之后,你需要重新进行信令协商(renegotiation),否则对端不会知道你的Track变了。这个我在后面的章节会详细讲。
常见问题与调试技巧
最后,分享几个我调试时常用的方法:
- 检查Track列表:
stream.getTracks()返回所有Track,可以用来确认你拿到了哪些轨道。 - 查看Track的label:
track.label可以告诉你这个Track来自哪个设备,比如"FaceTime HD Camera"或"Built-in Microphone"。 - 监听ended事件:当用户拔掉摄像头或关闭屏幕共享时,Track会触发ended事件。你可以监听这个事件来做UI提示。
track.onended = () => {
console.log('Track ended:', track.label);
// 更新UI,提示用户设备已断开
};
好了,关于MediaStream和MediaStreamTrack的基础就聊这么多。记住,它们是WebRTC的基石,搞懂了这两个概念,后面的路就好走了。