媒体流与轨道:MediaStream 与 MediaStreamTrack
媒体流与轨道: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 常用操作
创建空流
conststream=newMediaStream();// 空流,后面加轨道从已有轨道创建
conststream=newMediaStream([videoTrack,audioTrack]);获取轨道
// 所有轨道constallTracks=stream.getTracks();// 只视频轨constvideoTracks=stream.getVideoTracks();// 只音频轨constaudioTracks=stream.getAudioTracks();// 按 ID 取consttrack=stream.getTrackById(trackId);添加轨道
stream.addTrack(videoTrack);stream.addTrack(audioTrack);移除轨道
stream.removeTrack(videoTrack);克隆流
constnewStream=stream.clone();克隆出的新流和原流独立,轨道也是克隆的,互不影响。
流的活跃状态
if(stream.active){console.log('流是活跃的');}所有轨道都结束了,流就变成 inactive。
三、MediaStreamTrack 常用操作
开关轨道
// 关闭(画面变黑/静音)track.enabled=false;// 打开track.enabled=true;enabled = false 时轨道还在,还占带宽,只是发空数据。
停止轨道
track.stop();完全停止,释放设备。停止后轨道就废了,不能再用。
获取轨道信息
console.log(track.kind);// 'video' / 'audio'console.log(track.id);// 唯一IDconsole.log(track.label);// 设备名,比如'FaceTime HD Camera'console.log(track.readyState);// 'live' / 'ended'console.log(track.muted);// 是否被系统静音获取能力(Capabilities)
设备支持什么参数:
constcapabilities=videoTrack.getCapabilities();console.log(capabilities);// {// width: { min: 320, max: 1920 },// height: { min: 240, max: 1080 },// frameRate: { min: 1, max: 60 },// ...// }获取当前约束(Constraints)
当前用的什么参数:
constconstraints=videoTrack.getConstraints();获取当前设置(Settings)
实际生效的参数:
constsettings=videoTrack.getSettings();console.log(settings.width,settings.height,settings.frameRate);注意:getConstraints 是你请求的参数,getSettings 是实际生效的参数,可能不一样(设备不支持的话会降级)。
应用新约束
动态修改参数:
awaittrack.applyConstraints({width:640,height:480,frameRate:15});网络不好时降画质,网络好时升回去,都用这个。
克隆轨道
constnewTrack=track.clone();克隆一个一样的轨道,独立控制。
四、轨道事件
onmute / onunmute
轨道被系统静音/取消静音时触发:
track.onmute=()=>{console.log('轨道被静音了');};track.onunmute=()=>{console.log('轨道恢复了');};比如摄像头被其他程序占用了,就会 mute。
onended
轨道结束时触发:
track.onended=()=>{console.log('轨道结束了');};用户拔掉摄像头、调用 stop()、页面关闭都会触发。
onoverconstrained
约束条件设备满足不了时触发。
五、流的事件
onaddtrack / onremovetrack
流里添加/移除轨道时触发:
stream.onaddtrack=(event)=>{console.log('添加了轨道:',event.track.kind);};stream.onremovetrack=(event)=>{console.log('移除了轨道:',event.track.kind);};onactive / oninactive
流变成活跃/不活跃时触发。
六、常用操作技巧
1. 替换视频轨(切换摄像头)
functionreplaceVideoTrack(stream,newVideoTrack){// 移除旧的constoldTrack=stream.getVideoTracks()[0];if(oldTrack){stream.removeTrack(oldTrack);oldTrack.stop();}// 添加新的stream.addTrack(newVideoTrack);// 如果已经在 PeerConnection 里,还要替换发送的轨道// sender.replaceTrack(newVideoTrack);}2. 静音/取消静音
// 静音麦克风functionmuteAudio(stream){constaudioTrack=stream.getAudioTracks()[0];if(audioTrack){audioTrack.enabled=false;}}// 取消静音functionunmuteAudio(stream){constaudioTrack=stream.getAudioTracks()[0];if(audioTrack){audioTrack.enabled=true;}}3. 开关摄像头
functiontoggleCamera(stream){constvideoTrack=stream.getVideoTracks()[0];if(videoTrack){videoTrack.enabled=!videoTrack.enabled;returnvideoTrack.enabled;}}4. 合并两个流
把摄像头和屏幕共享合并到一个流:
functionmergeStreams(cameraStream,screenStream){constmerged=newMediaStream();// 屏幕视频screenStream.getVideoTracks().forEach(t=>merged.addTrack(t));// 摄像头音频(或者两个都加,看需求)cameraStream.getAudioTracks().forEach(t=>merged.addTrack(t));returnmerged;}5. 只取视频/只取音频
// 从流里只提取视频轨,生成新流functionvideoOnly(stream){returnnewMediaStream(stream.getVideoTracks());}// 只提取音频functionaudioOnly(stream){returnnewMediaStream(stream.getAudioTracks());}6. 拍照(从视频轨抓帧)
functiontakePhoto(videoTrack){constcanvas=document.createElement('canvas');constvideo=document.createElement('video');video.srcObject=newMediaStream([videoTrack]);// 等视频就绪returnnewPromise(resolve=>{video.onloadedmetadata=()=>{canvas.width=video.videoWidth;canvas.height=video.videoHeight;canvas.getContext('2d').drawImage(video,0,0);resolve(canvas.toDataURL('image/png'));};});}七、视频轨道的特殊操作
摄像头切换(移动端)
asyncfunctionswitchCamera(currentStream){constcurrentTrack=currentStream.getVideoTracks()[0];constfacingMode=currentTrack.getSettings().facingMode;constnewFacingMode=facingMode==='user'?'environment':'user';constnewStream=awaitnavigator.mediaDevices.getUserMedia({video:{facingMode:newFacingMode},audio:true});// 停掉旧的currentStream.getTracks().forEach(t=>t.stop());returnnewStream;}画质动态调整
根据网络情况动态改:
// 降画质asyncfunctionlowerQuality(track){awaittrack.applyConstraints({width:640,height:480,frameRate:15});}// 升画质asyncfunctionraiseQuality(track){awaittrack.applyConstraints({width:1280,height:720,frameRate:30});}配合 WebRTC 的带宽估计,自动调画质。
八、音频轨道的特殊操作
音量检测
用 Web Audio API 检测麦克风音量:
functiongetVolumeLevel(audioTrack){constaudioContext=newAudioContext();constsource=audioContext.createMediaStreamSource(newMediaStream([audioTrack]));constanalyser=audioContext.createAnalyser();source.connect(analyser);constdataArray=newUint8Array(analyser.frequencyBinCount);functiongetVolume(){analyser.getByteFrequencyData(dataArray);letsum=0;for(leti=0;i<dataArray.length;i++){sum+=dataArray[i];}returnsum/dataArray.length;}return{getVolume,audioContext};}可以做音量条、说话检测等功能。
音频处理
用 Web Audio API 可以加各种效果:
- 混响
- 均衡器
- 变声
- 降噪
处理完再通过 WebRTC 发出去。
九、PeerConnection 中的轨道
添加流
旧 API(还能用但不推荐):
pc.addStream(stream);添加轨道(推荐)
新 API,逐个轨道加:
pc.addTrack(videoTrack,stream);pc.addTrack(audioTrack,stream);第二个参数 stream 是关联的流,对方收到时轨道会在同一个流里。
替换轨道(切换摄像头时用)
通话中切换摄像头,不用重新协商:
constsender=pc.getSenders().find(s=>s.track.kind==='video');awaitsender.replaceTrack(newVideoTrack);replaceTrack 不用重新协商 SDP,切换很快很平滑。
接收端拿到轨道
pc.ontrack=(event)=>{conststream=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 候选、完整的连接建立流程。
我是黒漂技术佬。
