在 Vue 应用中用 VueUseuseDevicesList响应式枚举音视频设备
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
useDevicesList是 VueUse 提供的响应式封装,它将浏览器原生MediaDevices.enumerateDevicesAPI 转化为可响应式更新的设备列表,自动划分摄像头、麦克风与扬声器,并提供统一的权限申请能力。在本仓库的 AI 桌面与 Web 应用中,它被用于麦克风选择、语音流启动和音频设备异常处理,本文将以官方参考文档为主体,结合仓库源码讲解其用法、配置项与真实落地模式。
功能定位:Sensors 类别下的设备枚举 composable
在 .agents/skills/vueuse-functions/SKILL.md 中,useDevicesList被归类为Sensors(传感器)类别,功能描述为 "ReactiveenumerateDeviceslisting available input/output devices",即对MediaDevices.enumerateDevices()的响应式封装,用于列出可用的输入/输出设备,调用规则为AUTO(适用时自动使用)。
该 composable 的核心价值在于把以下繁琐工作自动化:
- 监听
devicechange事件并在设备插拔时自动刷新列表; - 将扁平化的
MediaDeviceInfo[]按kind自动划分为摄像头(videoinput)、麦克风(audioinput)、扬声器(audiooutput)三类; - 通过
ensurePermissions统一处理getUserMedia权限申请流程; - 暴露
isSupported用于检测浏览器是否支持该 API,保证 SSR 兼容。
基础用法:一行代码拿到三类设备
最基本的用法是从@vueuse/core导入并在组件或 composable 中调用:
import { useDevicesList } from '@vueuse/core' const { devices, videoInputs: cameras, audioInputs: microphones, audioOutputs: speakers, } = useDevicesList()返回值说明:
| 返回属性 | 类型 | 含义 |
|---|---|---|
devices | ShallowRef<MediaDeviceInfo[]> | 全部设备的完整列表 |
videoInputs | ComputedRef<MediaDeviceInfo[]> | 摄像头(kind === 'videoinput') |
audioInputs | ComputedRef<MediaDeviceInfo[]> | 麦克风(kind === 'audioinput') |
audioOutputs | ComputedRef<MediaDeviceInfo[]> | 扬声器(kind === 'audiooutput') |
permissionGranted | ShallowRef<boolean> | 媒体权限是否已授予 |
ensurePermissions | () => Promise<boolean> | 申请媒体权限,返回是否成功 |
isSupported | Ref<boolean> | 当前环境是否支持enumerateDevices |
解构时可以直接重命名为语义更清晰的变量名(如cameras、microphones、speakers),这在模板和业务逻辑中都非常直观。
请求权限:ensurePermissions与权限时序问题
浏览器在用户未授权前,enumerateDevices返回的设备对象的label和deviceId通常为空字符串,无法用于精确的设备选择。因此 VueUse 提供了ensurePermissions方法主动触发权限申请:
import { useDevicesList } from '@vueuse/core' const { ensurePermissions, permissionGranted, } = useDevicesList() await ensurePermissions() console.log(permissionGranted.value)ensurePermissions()内部会调用getUserMedia请求媒体权限,返回一个Promise<boolean>表示权限是否授予成功。调用后应再读取permissionGranted.value确认状态。
仓库源码 packages/stage-ui/src/composables/audio/audio-device.ts 中的askPermission函数给出了一个完整的权限申请封装,并指出了 VueUse 的一个时序细节:
async function askPermission() { try { const granted = await ensurePermissions() if (granted) { // NOTICE: // VueUse starts its post-permission device refresh without awaiting it, so callers can // otherwise observe the anonymous pre-permission list after askPermission() resolves. devices.value = await navigator.mediaDevices.enumerateDevices() } selectAvailableAudioInput() } catch (error) { // ...错误处理与埋点 } }源码注释明确说明:VueUse 在权限授予后会异步(不 await)刷新设备列表,因此ensurePermissions()resolve 后调用方可能仍读到权限授予前的匿名设备列表。仓库的做法是手动再调用一次原生navigator.mediaDevices.enumerateDevices()刷新devices.value,这一细节对实现"授权后立即拿到完整设备信息"的交互至关重要。
组件形式:UseDevicesList
除 composable 外,VueUse 还提供了同名组件形式,适合在模板中直接消费,通过v-slot暴露设备分组:
<template> <UseDevicesList v-slot="{ videoInputs, audioInputs, audioOutputs }"> Cameras: {{ videoInputs }} Microphones: {{ audioInputs }} Speakers: {{ audioOutputs }} </UseDevicesList> </template>在需要把设备列表交给子组件或直接渲染下拉选项的场景下,组件形式可以省去在<script>中手动桥接的样板代码。
配置项详解:UseDevicesListOptions
根据参考文档的类型声明,useDevicesList接受一个可选配置对象:
export interface UseDevicesListOptions extends ConfigurableNavigator { onUpdated?: (devices: MediaDeviceInfo[]) => void /** * Request for permissions immediately if it's not granted, * otherwise label and deviceIds could be empty * * @default false */ requestPermissions?: boolean /** * Request for types of media permissions * * @default { audio: true, video: true } */ constraints?: MediaStreamConstraints }| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
onUpdated | (devices: MediaDeviceInfo[]) => void | 无 | 设备列表每次更新后的回调 |
requestPermissions | boolean | false | 若设为true,composable 初始化时若权限未授予则立即申请;否则label和deviceId可能为空 |
constraints | MediaStreamConstraints | { audio: true, video: true } | 权限申请时请求的媒体类型,可只请求音频或视频 |
继承ConfigurableNavigator | — | window.navigator | 可传入自定义navigator对象(SSR / 测试场景) |
constraints的取舍直接影响用户体验:
- 默认
{ audio: true, video: true }会在申请权限时同时请求麦克风和摄像头授权,若应用只需要语音输入,用户可能会对"为什么请求摄像头权限"产生疑虑; - 仓库的 packages/stage-ui/src/composables/audio/audio-device.ts 与 apps/stage-web/src/composables/audio-input.ts 都传入了
constraints: { audio: true },即只申请音频权限,避免不必要的摄像头授权弹窗。
返回值详解:UseDevicesListReturn
参考文档的完整返回类型声明如下:
export interface UseDevicesListReturn extends Supportable { /** * All devices */ devices: ShallowRef<MediaDeviceInfo[]> videoInputs: ComputedRef<MediaDeviceInfo[]> audioInputs: ComputedRef<MediaDeviceInfo[]> audioOutputs: ComputedRef<MediaDeviceInfo[]> permissionGranted: ShallowRef<boolean> ensurePermissions: () => Promise<boolean> }继承的Supportable提供了isSupported属性,用于在调用enumerateDevices前检测浏览器支持度,避免在不支持的 WebView 或老版本浏览器中报错。这在多端项目中尤为重要——例如仓库的 stage-web / stage-pocket 等应用同时面向浏览器与 Capacitor WebView,运行环境差异明显。
仓库实战:从设备枚举到语音流的完整链路
仓库中最完整的实践案例位于 packages/stage-ui/src/composables/audio/audio-device.ts,它展示了useDevicesList与useUserMedia的组合使用模式:
1. 默认设备偏好解析
function resolvePreferredAudioInput(audioInputs: MediaDeviceInfo[]) { return audioInputs.find(device => device.deviceId === 'default')?.deviceId || audioInputs[0]?.deviceId || '' }优先选择deviceId === 'default'的系统默认麦克风,否则回退到列表第一个输入设备。
2. 响应式设备选项
const audioInputOptions = computed(() => audioInputs.value .filter(device => device.deviceId) .map(device => ({ label: device.label || device.deviceId, value: device.deviceId, })))将audioInputs映射为可直接供下拉选择组件消费的{ label, value }列表,未授权时label为空则回退展示deviceId。
3. 设备热插拔时自动纠正选择
watch(audioInputs, () => { selectAvailableAudioInput() })监听audioInputs变化(拔掉当前选中的麦克风时触发),若所选设备不再存在则自动切换到默认设备,保证语音流不会因设备移除而失效。
4. 精确设备约束 + 流启动失败回退
const deviceConstraints = computed<MediaStreamConstraints>(() => ({ audio: selectedAudioInput.value ? { deviceId: { exact: selectedAudioInput.value }, autoGainControl: true, echoCancellation: true, noiseSuppression: true, } : { autoGainControl: true, echoCancellation: true, noiseSuppression: true, }, }))这里展示了从设备枚举结果到getUserMedia精确约束的完整衔接:选定设备后用deviceId: { exact }锁定设备,同时开启自动增益、回声消除与降噪;启动失败时还会按NotFoundError/OverconstrainedError/ "Requested device not found" 判断设备缺失场景并逐级回退。
apps/stage-web与apps/stage-pocket下的 audio-input.ts 则提供了另一种更简洁的组织方式:用watch同时监听permissionGranted、audioInputs与selectedAudioInputId,在权限就绪且设备存在时自动完成设备选择,随后才允许media.start()启动语音流。
测试验证:如何 mockuseDevicesList
仓库的单元测试 packages/stage-ui/src/composables/audio/audio-device.test.ts 展示了在 Vitest 中 mock@vueuse/core的标准做法:
vi.mock('@vueuse/core', async () => { const { ref } = await import('vue') audioDeviceMock.audioInputsRef = ref([]) return { useDevicesList: () => ({ audioInputs: audioDeviceMock.audioInputsRef, permissionGranted: ref(false), ensurePermissions: audioDeviceMock.ensurePermissions, }), useUserMedia: () => ({ ... }), } })测试覆盖了两个关键行为:
askPermission()在权限被拒绝(DOMException,name === 'NotAllowedError')时,会以低基数的埋点事件上报permission_denied,且不暴露浏览器原始错误文本;- 设备列表为空时调用
askPermission()不会产生产品埋点事件。
通过 mockensurePermissions的 resolve/reject 分支,可以在无真实硬件环境下稳定验证权限链路与埋点逻辑,这对 CI 环境的可重复性至关重要。
使用注意与最佳实践小结
- SSR 与不支持环境:先检查
isSupported再调用ensurePermissions,避免在无navigator.mediaDevices的环境抛出异常; - 权限时序:
ensurePermissions()resolve 后设备列表的刷新可能尚未完成,必要时手动await navigator.mediaDevices.enumerateDevices()刷新(见 audio-device.ts 的 NOTICE 注释); - 最小权限原则:仅需语音时传入
constraints: { audio: true },不要把摄像头授权一并请求; - 设备热插拔:通过
watch(audioInputs, ...)监听设备列表变化,在所选设备消失时自动回退到默认设备; - 错误分类:区分
NotAllowedError(权限拒绝)与NotFoundError/OverconstrainedError(设备缺失),前者引导用户去浏览器设置授权,后者做设备回退或降级提示。
useDevicesList虽然只是一个轻量的枚举封装,但配合ensurePermissions、useUserMedia与响应式watch,足以支撑从设备枚举、权限申请、精确选麦到语音流启停的完整媒体输入链路,这也是它在当前仓库多端应用中被反复复用的根本原因。
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考