拓冰建站拓冰建站
首页 / 资讯中心 / 正文

VueUse useUserMedia 实战指南:在 airi 项目中构建响应式媒体流采集

VueUse useUserMedia 实战指南在 airi 项目中构建响应式媒体流采集【免费下载链接】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-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi导读useUserMedia是 VueUse 提供的对浏览器原生mediaDevices.getUserMedia与 stage-pocket的麦克风流管理。读完本文你将掌握useUserMedia的完整 API、设备切换autoSwitch机制、与useDevicesList的联动方式以及如何在真实项目中处理权限拒绝、设备失效等边界场景。useUserMedia 是什么useUserMedia的核心是响应式媒体流Reactive MediaStream它把navigator.mediaDevices.getUserMedia()的结果——一个MediaStream——放进 Vue 的ref中并暴露start、stop、restart三个生命周期方法让组件可以声明式地管理摄像头与麦克风。它的行为可以用一句话概括当enabled变为true时自动开始采集当constraints中的设备变化时自动重建流autoSwitch当组件卸载时自动释放流。相比直接手写getUserMedia与addEventListener(ended)的样板代码它消除了内存泄漏与重复获取流的隐患。浏览器 API 基础getUserMedia返回一个PromiseMediaStream它要求页面处于安全上下文HTTPS 或 localhost中并且会触发浏览器的权限提示。流中的MediaStreamTrack在设备被拔出、权限被撤销或标签页被切换时可能触发ended事件useUserMedia内部通过监听该事件来同步更新响应式状态。最小可运行示例把下面的代码放进任意 Vue 3 组件即可实现摄像头画面的实时预览script setup langts 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 refvideo / /template这段代码的核心链条是start()触发底层getUserMedia()→ 返回的MediaStream写入streamref →watchEffect响应式地把流挂到video元素的srcObject上完成预览。注意示例中省略了constraints此时getUserMedia会使用浏览器默认行为默认摄像头 默认麦克风这在仅需快速预览时是可行的如果需要指定设备请使用下文结合 useDevicesList 选择设备中的写法。API 与选项详解useUserMedia的完整类型声明如下节选自 .agents/skills/vueuse-functions/references/useUserMedia.mdexport interface UseUserMediaOptions extends ConfigurableNavigator { /** * If the stream is enabled * default false */ enabled?: MaybeRefboolean /** * Recreate stream when deviceIds or constraints changed * * default true */ autoSwitch?: MaybeRefboolean /** * MediaStreamConstraints to be applied to the requested MediaStream * If provided, the constraints will override videoDeviceId and audioDeviceId * * default {} */ constraints?: MaybeRefMediaStreamConstraints } export interface UseUserMediaReturn extends Supportable { stream: RefMediaStream | undefined start: () PromiseMediaStream | undefined stop: () void restart: () PromiseMediaStream | undefined constraints: RefMediaStreamConstraints | undefined enabled: ShallowRefboolean autoSwitch: ShallowRefboolean }选项Options逐一说明选项类型默认值说明enabledMaybeRefbooleanfalse是否启用采集。设为true时自动开始获取流设为false时自动停止并释放。可传 ref便于响应式控制autoSwitchMaybeRefbooleantrue当constraints或其中的deviceId变化时自动销毁旧流并重建新流。这是实现切换麦克风/摄像头的关键constraintsMaybeRefMediaStreamConstraints{}传给getUserMedia的约束对象。一旦提供会覆盖内部的videoDeviceId/audioDeviceId快捷参数返回值Return逐一说明返回项类型说明streamRefMediaStream \| undefined当前的媒体流。未启动时为undefined可响应式绑定到video/audio的srcObjectstart()() PromiseMediaStream \| undefined主动开始采集返回新流已经存在流时通常复用或重建stop()() void停止采集并释放所有轨道调用track.stop()restart()() PromiseMediaStream \| undefined停止当前流后重新按当前约束获取新流用于恢复异常中断的流constraintsRefMediaStreamConstraints \| undefined当前生效的约束的响应式引用外部可读enabledShallowRefboolean当前是否处于启用状态可响应式读写autoSwitchShallowRefboolean当前是否开启自动切换可响应式读写此外UseUserMediaReturn extends Supportable因此返回值还继承了 VueUse 的isSupported当前环境是否支持navigator.mediaDevices.getUserMedia等能力用于在不支持的浏览器上优雅降级。关于enabled的注意点enabled默认是false意味着仅仅调用useUserMedia()并不会自动开启摄像头——首次采集必须显式调用start()或把enabled设为true。这也是为什么在 airi 项目中总是先调用askPermission()/ensurePermissions()再调用start()把权限申请与流启动分成两个明确的阶段见下文项目实战。结合 useDevicesList 选择设备useDevicesList负责枚举可用设备摄像头 / 麦克风并管理权限状态useUserMedia负责真正拉流。两者组合的标准模式来自原文档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, } }) })这里有两个值得注意的细节requestPermissions: trueuseDevicesList会在初始化时主动申请媒体权限。注意权限授予前deviceId和label可能是空的隐私保护因此必须先请求权限再读取设备列表。reactive包裹的constraintsuseUserMedia的constraints是MaybeRefMediaStreamConstraints。把deviceId直接指向一个computed配合默认开启的autoSwitch: true当用户在下拉框中切换设备时约束变化会自动触发流重建——这就是响应式设备切换的完整闭环。设备选择的工程化封装airi 仓库在 packages/stage-ui/src/composables/audio/audio-device.ts 中提供了更高层的useAudioDevice封装它完整展示了生产级的设备选择逻辑const { devices, audioInputs, permissionGranted, ensurePermissions, } useDevicesList({ constraints: { audio: true }, requestPermissions: requestPermission, }) const selectedAudioInput refstring(audioInputs.value.find(device device.deviceId default)?.deviceId || ) const deviceConstraints computedMediaStreamConstraints(() ({ audio: selectedAudioInput.value ? { deviceId: { exact: selectedAudioInput.value }, autoGainControl: true, echoCancellation: true, noiseSuppression: true, } : { autoGainControl: true, echoCancellation: true, noiseSuppression: true, }, })) const { stream, stop: stopStream, start: startUserMediaStream } useUserMedia({ constraints: deviceConstraints, enabled: false, autoSwitch: true, })该封装的要点默认设备偏好resolvePreferredAudioInput优先选择deviceId default的系统默认麦克风找不到再回退到列表第一项见 audio-device.ts。音频质量约束无论是否指定设备都强制开启autoGainControl自动增益、echoCancellation回声消除、noiseSuppression降噪保证语音聊天质量。设备热插拔自适应通过watch(audioInputs, ...)监听设备列表变化当选中的麦克风消失时自动切换回默认/首个可用设备见 audio-device.ts。deviceId: { exact: ... }使用exact强制匹配精确设备避免浏览器在设备不可用时静默回退到其他设备。autoSwitch 与流生命周期autoSwitch是useUserMedia最值得深入理解的机制。其行为约定如下默认true只要constraints引用的响应式值发生变化例如deviceId从摄像头 A 变成摄像头 BuseUserMedia会先停止旧流停止所有 track再按新约束发起新的getUserMedia并把新流写入stream。设为false约束变化不会触发自动重建你需要手动调用restart()来应用新约束。airi 中 apps/stage-web/src/composables/audio-input.ts 展示了restart()的正确用法——当流已处于启用状态时切换设备需要先restart()再start()const media useUserMedia({ constraints, autoSwitch: true, enabled: false }) async function start() { await request() if (!devices.permissionGranted.value) return if (!selectedAudioInput.value) return if (media.enabled.value) { media.restart() } media.start() }这段代码回答了一个常见疑问为什么切换设备后画面/声音没变——因为在autoSwitch: true且流已存在时constraints变化虽然会触发流重建但如果你通过start()手动管理生命周期需要确保调用时机正确而在一些需要精确控制的场景如先释放再获取显式restart()是更保险的选择。组件卸载时的自动清理useUserMedia在组件卸载onScopeDispose时会自动stop()并释放所有轨道因此不需要在onUnmounted中手动调用stop()。这是它相比裸写getUserMedia在内存管理上的核心优势。权限管理请求、拒绝与错误归一化getUserMedia的权限流程涉及多个错误分支airi 的封装给出了完整的工程化处理。权限申请流程在 audio-device.ts 中权限申请被封装为askPermission()async function askPermission() { try { const granted await ensurePermissions() if (granted) { // VueUse 在授权后会异步刷新设备列表且不等待 // 这里手动枚举一次避免拿到授权前的匿名设备列表 devices.value await navigator.mediaDevices.enumerateDevices() } selectAvailableAudioInput() } catch (error) { const errorCode audioDeviceErrorCode(error) if (errorCode permission_denied) { trackMicrophonePermissionDenied({ stt_provider_id: unknown, error_code: errorCode }) } console.error(Error ensuring permissions:, error) throw error } }源码注释揭示了一个 VueUse 的已知行为坑useDevicesList.ensurePermissions()在授权后触发的设备列表刷新是异步的且不被 await因此askPermission()返回后可能拿到的仍是授权前的匿名设备列表。airi 的解决方案是手动调用navigator.mediaDevices.enumerateDevices()补一次刷新源码注释标注了该行为对应vueuse/core14.2.1 的实现见 audio-device.ts。错误分类与设备失效兜底airi 定义了audioDeviceErrorCode把浏览器错误归一化为两类低基数分析码见 audio-device.ts浏览器错误归一化结果NotAllowedError/PermissionDeniedErrorpermission_denied用户拒绝权限其他如NotFoundError、OverconstrainedErrordevice_unavailable设备不存在或约束不满足isMissingAudioInputDeviceError进一步识别设备已拔出/不可用类错误NotFoundError、OverconstrainedError、消息含Requested device not found用于启动流时的兜底重试见 audio-device.tsasync function startStream() { selectAvailableAudioInput() try { return await startUserMediaStream() } catch (error) { // 尝试切换到默认/首个可用设备后重试 const fallbackDeviceId resolvePreferredAudioInput(audioInputs.value) if (fallbackDeviceId fallbackDeviceId ! selectedAudioInput.value) { selectedAudioInput.value fallbackDeviceId await nextTick() return await startUserMediaStream() } // 设备确实不存在时清空选择后以默认约束重试 if (selectedAudioInput.value isMissingAudioInputDeviceError(error)) { selectedAudioInput.value await nextTick() return await startUserMediaStream() } throw error } }这一先精确设备 → 再默认设备 → 最后无约束兜底的三级降级策略是生产级媒体采集的重要经验不要把设备选择失败直接暴露给用户而是先尝试自动恢复。测试验证packages/stage-ui/src/composables/audio/audio-device.test.ts 用 Vitest mock 了vueuse/core的useDevicesList/useUserMedia验证了两个关键行为用户拒绝权限NotAllowedError时askPermission()会抛错且仅上报归一化后的error_code: permission_denied不泄露浏览器原始错误文案隐私与日志卫生。设备列表为空时不会触发无意义的分析事件。这提醒我们封装useUserMedia时错误处理既要兜底恢复也要对上报数据做归一化与最小化。在 airi 项目中的实际应用场景场景一实时语音输入的麦克风管理airi 的 Web 端apps/stage-web与移动端apps/stage-pocket各有一个useAudioInputcomposable两者结构一致见 apps/stage-web/src/composables/audio-input.ts 与 apps/stage-pocket/src/composables/audio-input.ts用于实时语音输入。其工作流为useDevicesList({ constraints: { audio: true }, requestPermissions: false })枚举麦克风但不立即申请权限。用户首次点击开始说话时request()内部调用ensurePermissions()申请权限。权限授予后监听permissionGranted与audioInputs自动选中第一个可用麦克风。media.start()启动useUserMedia流media.stop()停止。这种权限延迟到用户手势触发的设计比页面加载即请求权限更符合浏览器自动播放与权限策略的最佳实践。场景二音频录制调试工具apps/stage-web/src/pages/devtools/audio-record.vue 提供了一个完整的麦克风录制调试页展示了useDevicesList与媒体录制的配合const { audioInputs } useDevicesList({ constraints: { audio: true }, requestPermissions: true }) const constraintId ref() async function getMediaStreamTrack(constraint: ConstrainDOMString) { const stream await navigator.mediaDevices.getUserMedia({ audio: { deviceId: constraint } }) return stream.getAudioTracks()[0] }页面通过select v-modelconstraintId绑定麦克风下拉框把选中的deviceId作为约束传入录制为 WAV 格式并回放。这是useDevicesList最典型的展示型用法设备列表 → 用户选择 → 约束采集。场景三媒体流的进一步消费获取到的stream并不只能挂到video/audio上。airi 的 packages/stage-ui/src/components/gadgets/audio-spectrum.vue 展示了把MediaStream输入AudioContext做频谱可视化的模式const source audioContext.createMediaStreamSource(props.stream)对于实时语音聊天类应用麦克风流通常需要接入AudioContext、AudioWorklet或 WebRTC 的RTCPeerConnection.addTrack()理解useUserMedia输出的stream是一个标准MediaStream是打通这些链路的前提。常见问题与最佳实践小结问题原因与解法stream一直是undefinedenabled默认false需要显式调用start()或设置enabled: true且页面必须在安全上下文HTTPS/localhost中切换设备后流没变确认autoSwitch: true默认若手动管理生命周期先restart()再start()授权后拿到的设备列表是空的/匿名的VueUseensurePermissions()后设备刷新不被 await需手动navigator.mediaDevices.enumerateDevices()补充刷新airi 做法见 audio-device.ts用户拒绝权限或设备被拔出参考 airi 的三级降级策略精确设备失败 → 默认设备 → 无约束兜底并对错误归一化上报组件卸载后摄像头指示灯仍亮useUserMedia已在onScopeDispose中自动stop()释放轨道确认你使用的是 composable 的stream/start而非裸getUserMedia最佳实践可以总结为三点设备选择与流采集分离用useDevicesList管设备与权限用useUserMedia管流生命周期二者通过响应式constraints连接。约束永远开启音频增强语音场景下固定加autoGainControl、echoCancellation、noiseSuppression并视需要配合deviceId: { exact }。错误处理先恢复、再上报设备失效先自动回退重试确认失败后再以低基数、归一化的错误码记录避免把浏览器原始错误直接抛给用户或日志。延伸阅读本仓库内的参考文档useUserMedia.md同属 Sensors 分类的关联 composableuseDevicesList.md、usePermission.md生产级封装与测试packages/stage-ui/src/composables/audio/audio-device.ts、packages/stage-ui/src/composables/audio/audio-device.test.ts实际应用Web 端 apps/stage-web/src/composables/audio-input.ts、移动端 apps/stage-pocket/src/composables/audio-input.ts、录制调试页 apps/stage-web/src/pages/devtools/audio-record.vue【免费下载链接】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-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门