媒体流与轨道: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 常用操作

创建空流

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 候选、完整的连接建立流程。

我是黒漂技术佬。

相关推荐
KaifuZeng2 小时前
通信与接口协议面试十一、网口
嵌入式硬件·面试·通信与接口协议
ACP广源盛139246256732 小时前
破局 PCIe 4.0 交换瓶颈@ACP#IX8024 / ASM58024参数及应用对比
大数据·人工智能·分布式·嵌入式硬件
吠品3 小时前
Python字典键获取的四种实用方法
stm32·单片机·嵌入式硬件
VidDown3 小时前
在线视频下载器技术解析:原理、工具与合规实践
网络·音视频·实时音视频·视频编解码·视频下载
REDcker3 小时前
腾讯TRRO SDK深度剖析
音视频·webrtc·实时音视频·直播·rtc·腾讯
ye150127774553 小时前
220V转12V|18V1A非隔离电源芯片WT5110
单片机·嵌入式硬件·其他·硬件工程
XTIOT6664 小时前
CRPT 诚实标识采集落地技术实践:分工况硬件选型与合规数据标准化解决方案
大数据·运维·人工智能·嵌入式硬件·物联网
云栖梦泽4 小时前
从 IMX415 摄像头驱动理解 Media Controller 框架
linux·开发语言·c++·嵌入式硬件
国科安芯5 小时前
基于ASM1042S2S的箭载通信网络抗辐射加固方案研究
服务器·网络·嵌入式硬件·fpga开发·架构·信号处理
媒介发稿小能手5 小时前
权威+垂直+全域媒体资源:选对GEO发稿平台,抢占AI搜索高地
大数据·人工智能·媒体