媒体流与轨道: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 候选、完整的连接建立流程。

我是黒漂技术佬。