VueUse useUserMedia:响应式摄像头与麦克风流采集完全指南
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 相比,它解决了三个痛点:
- 响应式:
stream是一个Ref<MediaStream | undefined>,enabled是一个ShallowRef<boolean>,开启/关闭流、约束变更都可以由 Vue 的响应式系统驱动; - 生命周期自动清理:通过
tryOnScopeDispose在组件/作用域卸载时自动stop(),避免"摄像头灯还亮着"的资源泄漏; - 能力检测:内置
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>
这段代码展示了三个额外要点:
- 设备热插拔容错:
useDevicesList的onUpdated回调会在设备枚举更新后触发(源码中监听devicechange事件,见 packages/core/useDevicesList/index.ts#L114),当已选设备消失时自动回退到第一个可用摄像头; - 点击切换设备:通过修改
currentCamera(shallowRef),响应式地改变constraints.video.deviceId,随即触发autoSwitch重启流; - 按钮启停:
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() 释放设备
六、权限、安全与注意事项
- HTTPS 强制:
getUserMedia只在安全上下文(HTTPS 或 localhost)下可用,这是浏览器的硬性限制,开发调试时请通过localhost访问; - 权限流程:首次采集会触发浏览器授权弹窗。建议结合
usePermission(见 packages/core/usePermission/index.ts)查询'camera'/'microphone'权限状态,在拒绝时给出 UI 引导;useDevicesList的ensurePermissions()内部正是基于它实现的; - 设备 ID 稳定性:
deviceId在不同浏览器/会话间可能变化,切勿持久化硬编码;设备列表的label只有授权后才能获取; - 移动端与多标签页:同一设备同一时间通常只能被一个页面占用,切换标签页或重复
start()时注意处理NotReadableError/OverconstrainedError等异常(源码中_start未捕获错误,需调用方自行 try/catch); - 音频采集回授:预览时给
<video>加muted可避免扬声器声音回灌麦克风; - 性能:
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 自动重启三条响应式链路,你就能在真实项目中写出既简洁又健壮的媒体采集代码。