在 Vue 应用中用 VueUse `useDevicesList` 响应式枚举音视频设备
2026/9/10 11:31:35 网站建设 项目流程

在 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()

返回值说明:

返回属性类型含义
devicesShallowRef<MediaDeviceInfo[]>全部设备的完整列表
videoInputsComputedRef<MediaDeviceInfo[]>摄像头(kind === 'videoinput'
audioInputsComputedRef<MediaDeviceInfo[]>麦克风(kind === 'audioinput'
audioOutputsComputedRef<MediaDeviceInfo[]>扬声器(kind === 'audiooutput'
permissionGrantedShallowRef<boolean>媒体权限是否已授予
ensurePermissions() => Promise<boolean>申请媒体权限,返回是否成功
isSupportedRef<boolean>当前环境是否支持enumerateDevices

解构时可以直接重命名为语义更清晰的变量名(如camerasmicrophonesspeakers),这在模板和业务逻辑中都非常直观。

请求权限:ensurePermissions与权限时序问题

浏览器在用户未授权前,enumerateDevices返回的设备对象的labeldeviceId通常为空字符串,无法用于精确的设备选择。因此 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设备列表每次更新后的回调
requestPermissionsbooleanfalse若设为true,composable 初始化时若权限未授予则立即申请;否则labeldeviceId可能为空
constraintsMediaStreamConstraints{ audio: true, video: true }权限申请时请求的媒体类型,可只请求音频或视频
继承ConfigurableNavigatorwindow.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,它展示了useDevicesListuseUserMedia的组合使用模式:

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-webapps/stage-pocket下的 audio-input.ts 则提供了另一种更简洁的组织方式:用watch同时监听permissionGrantedaudioInputsselectedAudioInputId,在权限就绪且设备存在时自动完成设备选择,随后才允许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()在权限被拒绝(DOMExceptionname === 'NotAllowedError')时,会以低基数的埋点事件上报permission_denied,且不暴露浏览器原始错误文本
  • 设备列表为空时调用askPermission()不会产生产品埋点事件。

通过 mockensurePermissions的 resolve/reject 分支,可以在无真实硬件环境下稳定验证权限链路与埋点逻辑,这对 CI 环境的可重复性至关重要。

使用注意与最佳实践小结

  1. SSR 与不支持环境:先检查isSupported再调用ensurePermissions,避免在无navigator.mediaDevices的环境抛出异常;
  2. 权限时序ensurePermissions()resolve 后设备列表的刷新可能尚未完成,必要时手动await navigator.mediaDevices.enumerateDevices()刷新(见 audio-device.ts 的 NOTICE 注释);
  3. 最小权限原则:仅需语音时传入constraints: { audio: true },不要把摄像头授权一并请求;
  4. 设备热插拔:通过watch(audioInputs, ...)监听设备列表变化,在所选设备消失时自动回退到默认设备;
  5. 错误分类:区分NotAllowedError(权限拒绝)与NotFoundError/OverconstrainedError(设备缺失),前者引导用户去浏览器设置授权,后者做设备回退或降级提示。

useDevicesList虽然只是一个轻量的枚举封装,但配合ensurePermissionsuseUserMedia与响应式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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询