真正可行的方案是组合使用设备枚举、轨道控制和输出切换三类机制:耳机插拔需手动选择输出设备并调用setsinkid;摄像头热插拔靠ondevicechange触发后重新enumeratedevices比对变化;前后摄像头切换用applyconstraints动态换轨;静音与停显直接设track.enabled。

Web 页面无法直接监听耳机插拔,也不能靠“设备变动”准确感知摄像头物理开关。真正可行的方案是组合使用设备枚举、轨道控制和输出切换三类机制,各自解决不同场景。
耳机插拔:别信 ondevicechange,改用 sinkId 切换
浏览器根本收不到系统级耳机插拔事件。navigator.mediaDevices.ondevicechange 只在音频输出设备被系统识别为“新设备”时触发(比如蓝牙耳机首次配对),普通 3.5mm 插拔几乎不触发。可靠做法是让用户手动选择输出设备:
- 调用 enumerateDevices() 获取所有
audiooutput设备,过滤出 label 非空的项(如 “扬声器”、“Headphones”) - 给 audio 元素绑定按钮,点击后执行 setSinkId(deviceId) 切换输出目标
- 注意:仅 Chromium 内核浏览器(Chrome/Edge ≥81)支持,且必须 HTTPS 环境 + 用户已授予权限
- 失败时捕获
NotSupportedError(Safari/Firefox 不支持)、SecurityError(权限未获)或InvalidStateError(audio 元素未加载音源)
摄像头热插拔:靠 devicechange + enumerateDevices 刷新列表
USB 摄像头插拔、笔记本盖合上再打开等行为,会触发 ondevicechange 事件。但它只提示“设备可能变了”,不告诉你变在哪。必须立刻重新枚举:
如果你了解HTML,CSS和JavaScript,您已经拥有所需的工具开发Android应用程序。本动手本书展示了如何使用这些开源web标准设计和建造,可适应任何Android设备的应用程序 - 无需使用Java。您将学习如何创建一个在您选择的平台的Android友好的网络应用程序,然后转换与自由PhoneGap框架到一个原生的Android应用程序。了解为什么设备无关的移动应用是未来的潮流,并开始构建应用程序,提供更
- 设置
navigator.mediaDevices.ondevicechange = async () => { const devices = await navigator.mediaDevices.enumerateDevices(); } - 对比前后两次的
videoinputdeviceId 列表,判断新增、消失或 label 变更 - 若当前视频流依赖某个已消失的 deviceId,需主动停止旧轨道并重建流,否则出现黑屏或报错
- 首次枚举前建议先调一次
getUserMedia({ video: true }),确保能读取facingMode等能力信息
前后摄像头切换:不用重启流,用 applyConstraints 动态换轨
手机或双摄笔记本切换前后镜头,本质是换一个 videoinput 设备。推荐走动态约束路线,避免闪屏和权限反复申请:
- 从
stream.getVideoTracks()[0]获取当前视频轨道 - 调用 track.applyConstraints({ deviceId: { exact: "xxx" } }),其中 xxx 是目标摄像头的 deviceId
- 必须用
{ exact: "xxx" },写成{ deviceId: "xxx" }会被浏览器忽略 - 切换失败常见原因:目标设备正被占用、页面失去焦点、或旧 deviceId 已失效(插拔后需重新 enumerate 校验)
- 不支持该方法的旧浏览器(如 Firefox ≤79),降级为 stop 所有 track + 重新 getUserMedia
静音与停显:用 enabled 属性秒级响应
用户点“关麦克风”或“关摄像头”,不需要停流重开。直接操作轨道状态即可:
- audioTrack.enabled = false → 立即静音,远端听不到,本地预览也无声
- videoTrack.enabled = false → 画面冻结或变黑,但摄像头仍在工作,恢复快
- 该操作同步生效,无 Promise,适合高频点击场景
- 建议缓存 track 引用,避免每次重复调用
getAudioTracks()[0]
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!










