媒体流与轨道:MediaStream 与 MediaStreamTrack
采集到的音视频用 MediaStream 封装,里面包含多个 MediaStreamTrack。这篇讲流和轨道的关系、怎么操作轨道、怎么合并分流、怎么处理轨道事件,以及实际开发中的常用技巧。
大家好,我是黒漂技术佬。
getUserMedia 返回的是 MediaStream 对象,里面装着视频轨、音频轨。流和轨道是 WebRTC 里最基础的数据结构,搞懂它们才能灵活处理音视频。
这篇讲 MediaStream 和 MediaStreamTrack 的详细用法:添加移除轨道、克隆、事件、常用操作技巧。
一、基本概念
MediaStream(媒体流)
一个媒体流对象,可以包含多个轨道(Track)。
MediaStream
├─ MediaStreamTrack(视频轨)
├─ MediaStreamTrack(音频轨)
└─ ... 更多轨道
MediaStreamTrack(媒体轨道)
单一类型的媒体轨道,要么是视频,要么是音频。
- 一个摄像头 = 一个视频轨
- 一个麦克风 = 一个音频轨
- 屏幕共享 = 一个视频轨(+可选的系统音频轨)
轨道的属性
每个轨道有:
kind:'video' 或 'audio'id:唯一标识label:设备名称enabled:是否启用(true/false)readyState:'live' 或 'ended'muted:是否静音(只读,由系统决定)
二、MediaStream 常用操作
创建空流
javascript
const stream = new MediaStream(); // 空流,后面加轨道
从已有轨道创建
javascript
const stream = new MediaStream([videoTrack, audioTrack]);
获取轨道
javascript
// 所有轨道
const allTracks = stream.getTracks();
// 只视频轨
const videoTracks = stream.getVideoTracks();
// 只音频轨
const audioTracks = stream.getAudioTracks();
// 按 ID 取
const track = stream.getTrackById(trackId);
添加轨道
javascript
stream.addTrack(videoTrack);
stream.addTrack(audioTrack);
移除轨道
javascript
stream.removeTrack(videoTrack);
克隆流
javascript
const newStream = stream.clone();
克隆出的新流和原流独立,轨道也是克隆的,互不影响。
流的活跃状态
javascript
if (stream.active) {
console.log('流是活跃的');
}
所有轨道都结束了,流就变成 inactive。
三、MediaStreamTrack 常用操作
开关轨道
javascript
// 关闭(画面变黑/静音)
track.enabled = false;
// 打开
track.enabled = true;
enabled = false 时轨道还在,还占带宽,只是发空数据。
停止轨道
javascript
track.stop();
完全停止,释放设备。停止后轨道就废了,不能再用。
获取轨道信息
javascript
console.log(track.kind); // 'video' / 'audio'
console.log(track.id); // 唯一ID
console.log(track.label); // 设备名,比如'FaceTime HD Camera'
console.log(track.readyState); // 'live' / 'ended'
console.log(track.muted); // 是否被系统静音
获取能力(Capabilities)
设备支持什么参数:
javascript
const capabilities = videoTrack.getCapabilities();
console.log(capabilities);
// {
// width: { min: 320, max: 1920 },
// height: { min: 240, max: 1080 },
// frameRate: { min: 1, max: 60 },
// ...
// }
获取当前约束(Constraints)
当前用的什么参数:
javascript
const constraints = videoTrack.getConstraints();
获取当前设置(Settings)
实际生效的参数:
javascript
const settings = videoTrack.getSettings();
console.log(settings.width, settings.height, settings.frameRate);
注意:getConstraints 是你请求的参数,getSettings 是实际生效的参数,可能不一样(设备不支持的话会降级)。
应用新约束
动态修改参数:
javascript
await track.applyConstraints({
width: 640,
height: 480,
frameRate: 15
});
网络不好时降画质,网络好时升回去,都用这个。
克隆轨道
javascript
const newTrack = track.clone();
克隆一个一样的轨道,独立控制。
四、轨道事件
onmute / onunmute
轨道被系统静音/取消静音时触发:
javascript
track.onmute = () => {
console.log('轨道被静音了');
};
track.onunmute = () => {
console.log('轨道恢复了');
};
比如摄像头被其他程序占用了,就会 mute。
onended
轨道结束时触发:
javascript
track.onended = () => {
console.log('轨道结束了');
};
用户拔掉摄像头、调用 stop()、页面关闭都会触发。
onoverconstrained
约束条件设备满足不了时触发。
五、流的事件
onaddtrack / onremovetrack
流里添加/移除轨道时触发:
javascript
stream.onaddtrack = (event) => {
console.log('添加了轨道:', event.track.kind);
};
stream.onremovetrack = (event) => {
console.log('移除了轨道:', event.track.kind);
};
onactive / oninactive
流变成活跃/不活跃时触发。
六、常用操作技巧
1. 替换视频轨(切换摄像头)
javascript
function replaceVideoTrack(stream, newVideoTrack) {
// 移除旧的
const oldTrack = stream.getVideoTracks()[0];
if (oldTrack) {
stream.removeTrack(oldTrack);
oldTrack.stop();
}
// 添加新的
stream.addTrack(newVideoTrack);
// 如果已经在 PeerConnection 里,还要替换发送的轨道
// sender.replaceTrack(newVideoTrack);
}
2. 静音/取消静音
javascript
// 静音麦克风
function muteAudio(stream) {
const audioTrack = stream.getAudioTracks()[0];
if (audioTrack) {
audioTrack.enabled = false;
}
}
// 取消静音
function unmuteAudio(stream) {
const audioTrack = stream.getAudioTracks()[0];
if (audioTrack) {
audioTrack.enabled = true;
}
}
3. 开关摄像头
javascript
function toggleCamera(stream) {
const videoTrack = stream.getVideoTracks()[0];
if (videoTrack) {
videoTrack.enabled = !videoTrack.enabled;
return videoTrack.enabled;
}
}
4. 合并两个流
把摄像头和屏幕共享合并到一个流:
javascript
function mergeStreams(cameraStream, screenStream) {
const merged = new MediaStream();
// 屏幕视频
screenStream.getVideoTracks().forEach(t => merged.addTrack(t));
// 摄像头音频(或者两个都加,看需求)
cameraStream.getAudioTracks().forEach(t => merged.addTrack(t));
return merged;
}
5. 只取视频/只取音频
javascript
// 从流里只提取视频轨,生成新流
function videoOnly(stream) {
return new MediaStream(stream.getVideoTracks());
}
// 只提取音频
function audioOnly(stream) {
return new MediaStream(stream.getAudioTracks());
}
6. 拍照(从视频轨抓帧)
javascript
function takePhoto(videoTrack) {
const canvas = document.createElement('canvas');
const video = document.createElement('video');
video.srcObject = new MediaStream([videoTrack]);
// 等视频就绪
return new Promise(resolve => {
video.onloadedmetadata = () => {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0);
resolve(canvas.toDataURL('image/png'));
};
});
}
七、视频轨道的特殊操作
摄像头切换(移动端)
javascript
async function switchCamera(currentStream) {
const currentTrack = currentStream.getVideoTracks()[0];
const facingMode = currentTrack.getSettings().facingMode;
const newFacingMode = facingMode === 'user' ? 'environment' : 'user';
const newStream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: newFacingMode },
audio: true
});
// 停掉旧的
currentStream.getTracks().forEach(t => t.stop());
return newStream;
}
画质动态调整
根据网络情况动态改:
javascript
// 降画质
async function lowerQuality(track) {
await track.applyConstraints({
width: 640,
height: 480,
frameRate: 15
});
}
// 升画质
async function raiseQuality(track) {
await track.applyConstraints({
width: 1280,
height: 720,
frameRate: 30
});
}
配合 WebRTC 的带宽估计,自动调画质。
八、音频轨道的特殊操作
音量检测
用 Web Audio API 检测麦克风音量:
javascript
function getVolumeLevel(audioTrack) {
const audioContext = new AudioContext();
const source = audioContext.createMediaStreamSource(
new MediaStream([audioTrack])
);
const analyser = audioContext.createAnalyser();
source.connect(analyser);
const dataArray = new Uint8Array(analyser.frequencyBinCount);
function getVolume() {
analyser.getByteFrequencyData(dataArray);
let sum = 0;
for (let i = 0; i < dataArray.length; i++) {
sum += dataArray[i];
}
return sum / dataArray.length;
}
return { getVolume, audioContext };
}
可以做音量条、说话检测等功能。
音频处理
用 Web Audio API 可以加各种效果:
- 混响
- 均衡器
- 变声
- 降噪
处理完再通过 WebRTC 发出去。
九、PeerConnection 中的轨道
添加流
旧 API(还能用但不推荐):
javascript
pc.addStream(stream);
添加轨道(推荐)
新 API,逐个轨道加:
javascript
pc.addTrack(videoTrack, stream);
pc.addTrack(audioTrack, stream);
第二个参数 stream 是关联的流,对方收到时轨道会在同一个流里。
替换轨道(切换摄像头时用)
通话中切换摄像头,不用重新协商:
javascript
const sender = pc.getSenders().find(s => s.track.kind === 'video');
await sender.replaceTrack(newVideoTrack);
replaceTrack 不用重新协商 SDP,切换很快很平滑。
接收端拿到轨道
javascript
pc.ontrack = (event) => {
const stream = event.streams[0];
remoteVideo.srcObject = stream;
};
对方添加轨道后,这边触发 ontrack 事件。
十、常见坑
坑 1:enabled = false 不省带宽
很多人以为静音就不发数据了,其实还在发,只是发静音帧/黑屏帧。要省带宽得移除轨道或者用码率限制。
坑 2:stop 之后轨道就废了
调用 stop() 后轨道就结束了,不能再启用。要重新开得再 getUserMedia。
坑 3:克隆的轨道也要手动 stop
clone() 出来的轨道是独立的,原轨道 stop 了克隆的还在跑,记得都要释放。
坑 4:流里可以有多个视频轨
一个流可以加多个视频轨,但不是所有播放器都支持显示多个。一般一个流一个视频轨+一个音频轨。
坑 5:replaceTrack 不需要重新协商
切换摄像头用 replaceTrack 就行,不用重新 createOffer/Answer,体验好很多。
坑 6:iOS 上轨道行为不一样
iOS Safari 上,页面切后台后视频轨会暂停,回来可能要重新处理。
十一、本篇小结
- MediaStream = 容器,装多个 MediaStreamTrack
- MediaStreamTrack = 单个音/视频轨道,是最小操作单元
- 轨道操作:enabled 开关、stop 停止、applyConstraints 改参数、clone 克隆
- 流操作:addTrack/removeTrack、clone、getTracks
- 常用技巧:切换摄像头、静音、合并流、分流、拍照、音量检测
- PeerConnection 中用 addTrack 添加,replaceTrack 替换,ontrack 接收
- 注意:enabled 不省带宽,stop 后不可恢复,克隆的轨道要手动释放
下一篇讲RTCPeerConnection 连接建立:SDP 协商、ICE 候选、完整的连接建立流程。
我是黒漂技术佬。