3、媒体流基础:MediaStream API介绍、getUserMedia()方法详解、获取摄像头视频流

好,咱们今天来聊聊媒体流的基础。说白了,WebRTC 的一切音视频通信,起点都是 MediaStream。你想想看,没有流,你拿什么去传?

我个人习惯把 MediaStream 理解成一个“管道”,管道里流淌的是视频轨道和音频轨道。这个管道本身不复杂,复杂的是怎么把摄像头、麦克风的数据灌进去。而 getUserMedia(),就是那个打开水龙头的动作。

核心一句话:MediaStream 是容器,MediaStreamTrack 是内容。getUserMedia() 是获取内容的入口。

3.1 MediaStream API 概览

MediaStream 其实就是一个接口,代表同步的媒体数据流。我在项目中遇到过不少新手,以为 MediaStream 就是视频本身,其实不是。它更像是一个“遥控器”,让你可以控制轨道的开启、关闭,甚至动态增减轨道。

它的主要属性和方法,我列个表给你看,这样更直观:

属性/方法 说明 我用过的场景
id 流的唯一标识符(只读) 调试时区分多个流
active 流是否处于活跃状态(只读) 判断摄像头是否被占用
getVideoTracks() 获取所有视频轨道 拿到轨道后设置参数
getAudioTracks() 获取所有音频轨道 静音/取消静音操作
addTrack() 向流中添加轨道 动态共享屏幕时加视频轨
removeTrack() 从流中移除轨道 关闭某个摄像头画面
clone() 克隆当前流 很少用,但多路输出时有用

嗯,这里要注意:active 属性很关键。我曾经调试一个 bug,发现流明明拿到了,但画面就是黑的。后来一查,是 active 为 false,因为摄像头被另一个进程占用了。所以,拿到流之后,先检查一下 stream.active,能省不少排查时间。

3.2 getUserMedia() 方法详解

getUserMedia() 是 WebRTC 里最常用的 API 之一。它的签名很简单:

navigator.mediaDevices.getUserMedia(constraints)
  .then(function(stream) {
    // 成功回调
  })
  .catch(function(err) {
    // 失败回调
  });

但简单归简单,坑可不少。我一个个说。

3.2.1 constraints 参数

constraints 是一个对象,用来指定你要获取什么类型的媒体。最基本的写法:

// 只获取视频
const constraints = {
  video: true,
  audio: false
};

// 只获取音频
const constraints = {
  video: false,
  audio: true
};

// 都获取
const constraints = {
  video: true,
  audio: true
};

但实际项目中,你很少会只写一个 true。为什么?因为你需要控制分辨率、帧率、甚至摄像头方向。比如:

const constraints = {
  video: {
    width: { ideal: 1280 },
    height: { ideal: 720 },
    frameRate: { ideal: 30 }
  },
  audio: {
    echoCancellation: true,
    noiseSuppression: true
  }
};

这里有个细节:ideal 表示理想值,浏览器会尽量满足,但不保证。如果你非要某个值不可,用 exact

const constraints = {
  video: {
    width: { exact: 1920 },
    height: { exact: 1080 }
  }
};

警告:使用 exact 要小心。如果设备不支持该分辨率,getUserMedia() 会直接抛错。我曾经在某个老旧笔记本上测试,指定 4K 分辨率,结果直接挂了。建议先用 ideal,或者通过 enumerateDevices() 先查一下设备能力。

3.2.2 返回值

getUserMedia() 返回的是一个 Promise。成功时 resolve 一个 MediaStream 对象,失败时 reject 一个错误。

常见的错误类型:

  • NotAllowedError:用户拒绝了权限。这个最常见,尤其是第一次访问时浏览器会弹窗询问。
  • NotFoundError:没有找到指定的设备。比如你要求摄像头,但设备没有。
  • NotReadableError:设备被占用。比如另一个应用正在使用摄像头。
  • OverconstrainedError:约束条件无法满足。比如你要求 4K 但设备不支持。
  • AbortError:未知错误,通常很少见。

小技巧:我习惯在 catch 里根据错误类型给用户不同的提示。比如 NotAllowedError 就引导用户去浏览器设置里开启权限,NotFoundError 就提示检查设备连接。别只弹一个「获取失败」,用户体验太差了。

3.3 获取摄像头视频流实战

理论说完了,咱们直接上代码。下面是一个完整的示例,从获取流到显示在页面上:

<!DOCTYPE html>
<html>
<body>
  <video id="localVideo" autoplay playsinline></video>
  <button id="startBtn">开启摄像头</button>
  <button id="stopBtn">关闭摄像头</button>

  <script>
    let localStream = null;

    document.getElementById('startBtn').onclick = async () => {
      try {
        const constraints = {
          video: {
            width: { ideal: 1280 },
            height: { ideal: 720 },
            frameRate: { ideal: 30 }
          },
          audio: false
        };

        localStream = await navigator.mediaDevices.getUserMedia(constraints);
        const video = document.getElementById('localVideo');
        video.srcObject = localStream;

        // 检查流是否活跃
        if (!localStream.active) {
          console.warn('流未激活,可能被占用');
        }

        // 打印轨道信息
        const tracks = localStream.getVideoTracks();
        console.log(`获取到 ${tracks.length} 个视频轨道`);
        tracks.forEach(track => {
          console.log(`轨道: ${track.label}, 状态: ${track.readyState}`);
        });

      } catch (err) {
        console.error('获取摄像头失败:', err);
        if (err.name === 'NotAllowedError') {
          alert('请允许摄像头权限');
        } else if (err.name === 'NotFoundError') {
          alert('未检测到摄像头设备');
        } else {
          alert('获取失败: ' + err.message);
        }
      }
    };

    document.getElementById('stopBtn').onclick = () => {
      if (localStream) {
        localStream.getTracks().forEach(track => track.stop());
        document.getElementById('localVideo').srcObject = null;
        localStream = null;
        console.log('摄像头已关闭');
      }
    };
  </script>
</body>
</html>

这段代码我解释几个关键点:

  • autoplay 和 playsinline:视频标签必须加这两个属性,否则在移动端 Safari 上不会自动播放。
  • srcObject:直接把 MediaStream 赋值给 video 标签,而不是用 src 属性。这是标准做法。
  • track.stop():关闭摄像头时,要停止所有轨道,并释放资源。否则摄像头灯会一直亮着。

避坑指南:我曾经在关闭摄像头时只调了 stream.stop(),但发现旧版浏览器不支持这个方法。后来统一改用 track.stop(),兼容性更好。另外,记得把 srcObject 置为 null,否则 video 标签还会显示最后一帧画面。

3.4 知识体系结构图

下面我用一张 SVG 图来梳理本章的核心逻辑,方便你理解整个流程:

MediaStream 获取流程 用户点击“开启” navigator.mediaDevices.getUserMedia() 成功:返回 MediaStream 对象 失败:返回错误信息 赋值给 video.srcObject 检查 active 状态 / 打印轨道信息 根据错误类型提示用户 NotAllowedError / NotFoundError 等

这张图把整个流程串起来了。从用户操作开始,到调用 API,再到成功或失败的处理。你写代码的时候,脑子里可以过一遍这个流程,不容易漏掉分支。

3.5 关于轨道的一些补充

拿到 MediaStream 之后,你可能会想对轨道做更多操作。比如:

  • 启用/禁用轨道track.enabled = false 可以“静音”视频或音频,但不会释放设备。适合做临时开关。
  • 获取轨道设置track.getSettings() 返回当前轨道的实际参数,比如实际分辨率、帧率。这个在调试时很有用。
  • 监听轨道结束track.onended 事件,当设备被拔出或权限被撤销时触发。

举个例子,我曾在项目中用 track.getSettings() 来检测用户用的是前置还是后置摄像头:

const track = stream.getVideoTracks()[0];
const settings = track.getSettings();
console.log('当前摄像头:', settings.facingMode);
// 输出可能是 'user'(前置)或 'environment'(后置)

嗯,这个 facingMode 属性在移动端特别实用。你可以根据它来切换 UI 上的翻转按钮图标。

个人经验:我建议你在开发时,把 track.getSettings() 的结果打印到控制台。很多时候你以为摄像头输出的是 720p,实际可能只有 480p。浏览器会根据性能自动降级,别被 ideal 值骗了。

好了,关于 MediaStream 和 getUserMedia 的基础就聊到这里。记住几个关键点:constraints 要合理设置、错误处理要细致、轨道操作要记得释放资源。下一节我们会深入聊聊如何切换摄像头和调整分辨率,到时候这些基础就派上用场了。


公众号:蓝海资料掘金营,微信deep3321