VueUse useUserMedia:响应式摄像头与麦克风流采集完全指南

原创2026-10-02 21:40:071,159 阅读
文章标签:前端

VueUse useUserMedia:响应式摄像头与麦克风流采集完全指南

导读

useUserMedia 是 VueUse 中面向 WebRTC 媒体采集场景的核心组合式函数,它把浏览器原生的 navigator.mediaDevices.getUserMedia 封装为 Vue 响应式 API,让你可以用声明式的方式获取摄像头/麦克风实时媒体流(MediaStream)、在 <video> 上实时预览、动态切换采集设备,并在组件卸载时自动释放设备占用。读完本篇,你将掌握 useUserMedia 的完整配置参数、返回值语义、与 useDevicesList 配合实现摄像头/麦克风切换的实战方案,以及其底层源码的响应式驱动原理。

一、useUserMedia 是什么

useUserMedia 位于 packages/core/useUserMedia/index.ts,其官方定位是 Reactive mediaDevices.getUserMedia streaming——即把一次性、基于 Promise 的原生 API 转变成可响应式启停、可自动切换的媒体流。

  • 类别:Sensors(传感器/设备类功能)
  • 关联功能:useDevicesList(枚举设备)、usePermission(权限查询)
  • 实现来源:该实现移植自 vue-use-web(作者 Abdelrahman Awad),源码开头有明确注释说明

与原生 API 相比,它解决了三个痛点:

  1. 响应式:stream 是一个 Ref<MediaStream | undefined>,enabled 是一个 ShallowRef<boolean>,开启/关闭流、约束变更都可以由 Vue 的响应式系统驱动;
  2. 生命周期自动清理:通过 tryOnScopeDispose 在组件/作用域卸载时自动 stop(),避免"摄像头灯还亮着"的资源泄漏;
  3. 能力检测:内置 isSupported,在不支持 getUserMedia 的环境(如部分非 HTTPS 页面、旧浏览器)中安全降级。

二、基础用法:把媒体流渲染到 video 元素

原文档给出的最小可用示例完整如下,它演示了"启动采集 + 预览到 <video>"的核心链路:

<script setup lang="ts">
import { useUserMedia } from '@vueuse/core'
import { useTemplateRef, watchEffect } from 'vue'

const { stream, start } = useUserMedia()
start()

const videoRef = useTemplateRef('video')
watchEffect(() => {
  // preview on a video element
  videoRef.value.srcObject = stream.value
})
</script>

<template>
  <video ref="video" />
</template>

几个要点:

  • useUserMedia() 默认不会自动开启采集(enabled 默认 false),因此需要显式调用 start();
  • stream 是响应式引用,watchEffect 会把最新的 MediaStream 赋给 video 元素的 srcObject。在真实页面中,建议为 <video> 加上 autoplay、playsinline、muted 等属性以优化移动端体验——可参考官方演示 packages/core/useUserMedia/demo.vue 中 <video ref="video" muted autoplay controls> 的写法;
  • start() 返回 Promise<MediaStream | undefined>,调用后会同步把 enabled 置为 true(源码见下文)。

三、完整配置参数与返回值

3.1 选项(UseUserMediaOptions)

依据 packages/core/useUserMedia/index.ts#L11-L30,选项接口定义如下:

参数 类型 默认值 说明
enabled MaybeRef<boolean> false 是否启用媒体流。为 true 时自动开始采集,为 false 时停止并释放轨道
autoSwitch MaybeRef<boolean> true 当 deviceId 或 constraints 变化时是否自动重建(restart)媒体流
constraints MaybeRef<MediaStreamConstraints> {} 传给 getUserMedia 的约束。若提供,将覆盖内部的 video/audio 设备选项
navigator Navigator 全局 navigator 自定义 navigator 实例,用于 iframe 或测试环境(来自 packages/core/_configurable.ts 的 ConfigurableNavigator)

关于 constraints 需要特别说明:useUserMedia 内部会从 constraints.video 与 constraints.audio 中提取设备约束传入 getUserMedia,因此你可以通过 deviceId: { exact: deviceId } 或 deviceId: deviceId(宽松匹配)来指定具体设备;若 constraints.video 为 false 则表示不采集视频,audio 同理。constraints 支持传入 reactive 对象,配合 autoSwitch 即可实现"切换设备即自动重启流"的响应式行为。

3.2 返回值(UseUserMediaReturn)

依据 packages/core/useUserMedia/index.ts#L32-L40:

返回值 类型 说明
stream Ref<MediaStream | undefined> 当前媒体流,未开启时为 undefined
start () => Promise<MediaStream | undefined> 手动开启采集,成功后置 enabled = true
stop () => void 停止所有轨道并置 enabled = false
restart () => Promise<MediaStream | undefined> 先停止再重新开启,用于应用新约束
constraints Ref<MediaStreamConstraints | undefined> 当前生效的约束引用(响应式)
enabled ShallowRef<boolean> 采集开关状态,可直接赋值控制启停
autoSwitch ShallowRef<boolean> 自动切换开关状态
isSupported ComputedRef<boolean> 环境能力检测结果,来自 Supportable 接口(见 packages/core/types.ts)

值得注意的是 enabled 是可写的:官方演示 packages/core/useUserMedia/demo.vue 中就是直接用一个按钮切换 enabled = !enabled 来控制启停,无需手动调用 start()/stop(),这正是响应式设计的体现。

四、设备选择实战:useUserMedia + useDevicesList

原文档第二段示例展示了如何与 useDevicesList 配合,动态指定摄像头和麦克风:

import { useDevicesList, useUserMedia } from '@vueuse/core'
import { computed, reactive } from 'vue'

const {
  videoInputs: cameras,
  audioInputs: microphones,
} = useDevicesList({
  requestPermissions: true,
})
const currentCamera = computed(() => cameras.value[0]?.deviceId)
const currentMicrophone = computed(() => microphones.value[0]?.deviceId)

const { stream } = useUserMedia({
  constraints: reactive({
    video: { deviceId: currentCamera },
    audio: { deviceId: currentMicrophone, }
  })
})

要点分析:

  • useDevicesList 是对 mediaDevices.enumerateDevices 的响应式封装(见 packages/core/useDevicesList/index.ts),返回 videoInputs、audioInputs、audioOutputs、devices、permissionGranted 等;
  • requestPermissions: true 是关键前提:在未授权的情况下,设备列表的 label 与 deviceId 会是空的。源码中 ensurePermissions() 会在权限未授予时通过 usePermission 查询,并临时调用 getUserMedia 触发浏览器授权弹窗(见 packages/core/useDevicesList/index.ts#L75-L108);
  • constraints 用 reactive() 包裹后,deviceId 的 computed 值变化会触发约束更新,而 useUserMedia 的 autoSwitch(默认 true)会自动 restart() 以应用新设备;
  • 注意原文档示例用了 { deviceId: currentCamera }(宽松匹配);如果需要精确绑定某个具体设备,可像官方演示那样写成 { deviceId: { exact: currentCamera } }。

官方演示的完整交互逻辑

在 packages/core/useUserMedia/demo.vue 中,官方提供了一个更完整的交互版本,值得参考:

<script setup lang="ts">
import { useDevicesList, useUserMedia } from '@vueuse/core'
import { reactive, shallowRef, useTemplateRef, watchEffect } from 'vue'

const currentCamera = shallowRef<string>()
const { videoInputs: cameras } = useDevicesList({
  requestPermissions: true,
  onUpdated() {
    if (!cameras.value.some(i => i.deviceId === currentCamera.value))
      currentCamera.value = cameras.value[0]?.deviceId
  },
})

const video = useTemplateRef('video')
const { stream, enabled } = useUserMedia({
  constraints: reactive({ video: { deviceId: { exact: currentCamera } } }),
})

watchEffect(() => {
  if (video.value)
    video.value.srcObject = stream.value!
})
</script>

<template>
  <div class="flex flex-col gap-4 text-center">
    <div>
      <button @click="enabled = !enabled">
        {{ enabled ? 'Stop' : 'Start' }}
      </button>
    </div>

    <div>
      <div
        v-for="camera of cameras"
        :key="camera.deviceId"
        class="px-2 py-1 cursor-pointer"
        :class="{ 'text-primary': currentCamera === camera.deviceId }"
        @click="currentCamera = camera.deviceId"
      >
        {{ camera.label }}
      </div>
    </div>
    <div>
      <video ref="video" muted autoplay controls class="h-100 w-auto" />
    </div>
  </div>
</template>

这段代码展示了三个额外要点:

  1. 设备热插拔容错:useDevicesList 的 onUpdated 回调会在设备枚举更新后触发(源码中监听 devicechange 事件,见 packages/core/useDevicesList/index.ts#L114),当已选设备消失时自动回退到第一个可用摄像头;
  2. 点击切换设备:通过修改 currentCamera(shallowRef),响应式地改变 constraints.video.deviceId,随即触发 autoSwitch 重启流;
  3. 按钮启停:enabled = !enabled 直接驱动流的开启与释放。

五、源码原理:响应式驱动的媒体流生命周期

理解 packages/core/useUserMedia/index.ts 的核心实现,有助于你在复杂场景下正确使用它。

5.1 能力检测

const isSupported = useSupported(() => navigator?.mediaDevices?.getUserMedia)

useSupported(见 packages/core/useSupported/index.ts)返回一个基于组件挂载状态计算的 ComputedRef<boolean>,在 SSR/服务端环境会得到 false,因此 useUserMedia 可以安全地在同构应用中调用而不会抛错。后续 _start() 内部也会以 if (!isSupported.value || stream.value) return 做双重防护。

5.2 响应式启停:watch enabled

watch(
  enabled,
  (v) => {
    if (v)
      _start()
    else _stop()
  },
  { immediate: true },
)

enabled 是内部状态的"总开关":无论你是调用 start()/stop() 还是直接赋值 enabled,最终都收敛到这条 watch 上。immediate: true 使得组件挂载即执行一次初始判断。

  • _start() 实际执行 navigator.mediaDevices.getUserMedia({ video: getDeviceOptions('video'), audio: getDeviceOptions('audio') }),并通过 getDeviceOptions 从 constraints 中提取 video/audio 约束(取不到则回退为 false,即不采集该类型);
  • _stop() 会遍历 stream.getTracks() 逐一调用 track.stop() 后置空 stream——这能真正关闭摄像头指示灯、释放麦克风占用,是避免资源泄漏的关键。

5.3 自动切换:watch constraints

watch(
  constraints,
  () => {
    if (autoSwitch.value && stream.value)
      restart()
  },
  { immediate: true, deep: true },
)

constraints 使用 deepRef(深度响应式 ref)持有,配合 deep: true 监听,可捕捉 deviceId 等嵌套字段的变化。当流已存在(stream.value 非空)且 autoSwitch 为 true 时,自动 restart()——即先停止旧流再按新约束重新采集。这就是"切换设备无需手动处理"的底层机制。若某场景希望约束变化不打断当前流,可显式传入 autoSwitch: false。

5.4 自动清理

tryOnScopeDispose(() => {
  stop()
})

组件卸载、getCurrentScope 作用域销毁时会自动调用 stop(),确保媒体流被释放,避免"离开页面摄像头仍被占用"的常见问题。

5.5 完整调用链小结

start() / enabled = true
  └─ watch(enabled) → _start()
       └─ navigator.mediaDevices.getUserMedia(constraints)
            └─ stream.value 更新 → watchEffect 同步到 video.srcObject

constraints.deviceId 变化
  └─ watch(constraints, {deep:true}) → restart() → _stop() → start()

组件卸载 / scope 销毁
  └─ tryOnScopeDispose → stop() → 各 track.stop() 释放设备

六、权限、安全与注意事项

  1. HTTPS 强制:getUserMedia 只在安全上下文(HTTPS 或 localhost)下可用,这是浏览器的硬性限制,开发调试时请通过 localhost 访问;
  2. 权限流程:首次采集会触发浏览器授权弹窗。建议结合 usePermission(见 packages/core/usePermission/index.ts)查询 'camera' / 'microphone' 权限状态,在拒绝时给出 UI 引导;useDevicesList 的 ensurePermissions() 内部正是基于它实现的;
  3. 设备 ID 稳定性:deviceId 在不同浏览器/会话间可能变化,切勿持久化硬编码;设备列表的 label 只有授权后才能获取;
  4. 移动端与多标签页:同一设备同一时间通常只能被一个页面占用,切换标签页或重复 start() 时注意处理 NotReadableError/OverconstrainedError 等异常(源码中 _start 未捕获错误,需调用方自行 try/catch);
  5. 音频采集回授:预览时给 <video> 加 muted 可避免扬声器声音回灌麦克风;
  6. 性能:stream 是 shallowRef,避免对 MediaStream 内部做深度响应式代理,保持与原生对象一致的引用语义(源码见 packages/core/useUserMedia/index.ts#L55)。

七、常见场景速查

只采集音频(如录音应用):

const { stream, start, stop } = useUserMedia({
  constraints: { video: false, audio: true },
})

采集视频但不用设备约束(如录屏窗口流场景类比):

const { stream, enabled } = useUserMedia({
  constraints: { video: { width: { ideal: 1280 }, height: { ideal: 720 } }, audio: false },
})

关闭自动切换,手动控制重启时机:

const { stream, constraints, restart } = useUserMedia({
  autoSwitch: false,
  constraints: reactive({ video: { deviceId: currentCamera } }),
})

// 需要应用新约束时手动调用
restart()

以上场景均可在 VueUse 文档站对应页面与 packages/core/useUserMedia/index.ts、packages/core/useDevicesList/index.ts 源码中进一步验证细节。useUserMedia 已通过 packages/core/index.ts 从 @vueuse/core 统一导出,可直接从该包导入使用。

结语

useUserMedia 把 Web 媒体采集从"一段段 Promise 回调 + 手动管理轨道"变成"响应式状态 + 自动生命周期",配合 useDevicesList 的设备枚举与 usePermission 的权限查询,足以支撑摄像头预览、设备切换、录音、直播推流前置采集等绝大多数 Vue 3 媒体应用场景。理解其 enabled 驱动、constraints 深度监听与 autoSwitch 自动重启三条响应式链路,你就能在真实项目中写出既简洁又健壮的媒体采集代码。

登录后查看全文
vueuse