更新记录

1.4.1(2026-07-30)

修改说明

1.4.0(2026-07-29)

  • 对完全相同的设备列表进行签名去重,避免页面启动时重复触发 deviceschange
  • 修复旋转角度后拍照比例不正确的问题
  • 新增 statechange 事件,在事件中返回 ready、opened、recording、format、rotation、mirror。

平台兼容性

uni-app

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
- - - - - 5.0 × ×
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - - -

uni-app x

Chrome Safari Android iOS 鸿蒙 微信小程序
- - 5.0 × × -

uni-app UVC USB 摄像头插件

基于 nvue 原生组件的 UVC USB 摄像头插件,可在 uni-app App 端完成 USB 摄像头发现、授权、画面预览、拍照、录像、分辨率切换、图像旋转和镜像等操作。

功能特性

  • 自动发现 UVC USB 摄像头
  • USB 设备访问权限申请
  • 相机、麦克风运行时权限处理
  • MJPEG、YUYV 预览格式切换
  • 自定义预览宽度、高度和帧率
  • 获取设备支持的分辨率列表
  • 运行时切换预览分辨率
  • 拍照并返回本地文件路径
  • 视频录制,可选择是否录制麦克风声音
  • 支持 0°、90°、180°、270° 旋转
  • 支持水平镜像
  • 获取组件当前运行状态
  • 提供日志、状态和错误事件
  • 页面隐藏时关闭摄像头,页面销毁时释放资源

运行环境

  • uni-app App 项目
  • nvue 页面
  • Android 真机
  • 支持 UVC 协议的 USB 摄像头
  • 手机或平板支持 USB Host/OTG
  • 已正确安装并配置本插件

建议使用真机调试。连接摄像头前,请确认设备已开启 OTG,并使用支持数据传输的转接线。

安装与配置

  1. 将插件导入 uni-app 项目。
  2. 按插件包要求完成原生插件或 UTS 插件配置。
  3. nvue 页面中直接使用 <uvc-camera> 组件。
  4. 重新制作自定义调试基座或云打包后,在 Android 真机运行。

插件涉及 USB、相机和可选的麦克风能力。具体 Android 权限与 manifest.json 配置应以插件包内的原生配置要求为准。

快速开始

1. 添加摄像头组件

<template>
  <view class="page">
    <uvc-camera
      ref="uvc"
      class="preview"
      :auto-start="true"
      :auto-request-permission="true"
      :preview-width="1920"
      :preview-height="1080"
      :preview-fps="30"
      format="mjpeg"
      :bandwidth-quirk="true"
      :rotation="90"
      :mirror="false"
      @deviceschange="onDevicesChange"
      @sizeschange="onSizesChange"
      @opened="onOpened"
      @closed="onClosed"
      @photosaved="onPhotoSaved"
      @recordingstart="onRecordingStart"
      @recordingsaved="onRecordingSaved"
      @statechange="onStateChange"
      @error="onError"
    />
  </view>
</template>

<script>
export default {
  data() {
    return {
      devices: [],
      sizes: [],
      recording: false,
      photoPath: ''
    }
  },

  methods: {
    camera() {
      return this.$refs.uvc
    },

    // 兼容普通对象、event.detail 和 UTS Map 风格事件数据。
    value(event, key, fallback) {
      if (!event) return fallback

      let data = event
      if (event.detail != null) {
        data = event.detail
      } else if (typeof event.get === 'function') {
        const detail = event.get('detail')
        if (detail != null) data = detail
      }

      if (data && typeof data.get === 'function') {
        const result = data.get(key)
        return result == null ? fallback : result
      }

      if (data && data[key] != null) return data[key]
      return fallback
    },

    refreshDevices() {
      this.camera().refreshDevices()
    },

    openFirstDevice() {
      if (this.devices.length === 0) {
        uni.showToast({ title: '未发现 USB 摄像头', icon: 'none' })
        return
      }

      this.camera().open(Number(this.devices[0].deviceId))
    },

    takePhoto() {
      this.camera().takePhotoAuto()
    },

    startRecording() {
      // false:不录制麦克风;true:录制麦克风并按需申请录音权限。
      this.camera().startRecordingAuto(false)
    },

    stopRecording() {
      this.camera().stopRecording()
    },

    closeCamera() {
      this.camera().close()
    },

    onDevicesChange(event) {
      this.devices = this.value(event, 'devices', [])
    },

    onSizesChange(event) {
      this.sizes = this.value(event, 'sizes', [])
    },

    onOpened(event) {
      const device = this.value(event, 'device', {})
      const size = this.value(event, 'size', {})
      console.log('摄像头已打开', device, size)
    },

    onClosed() {
      this.recording = false
    },

    onPhotoSaved(event) {
      const path = String(this.value(event, 'path', '') || '')
      this.photoPath = path

      if (path) {
        uni.previewImage({
          urls: [`file://${path}`]
        })
      }
    },

    onRecordingStart(event) {
      this.recording = true
      console.log('开始录像', this.value(event, 'path', ''))
    },

    onRecordingSaved(event) {
      this.recording = false
      console.log('录像已保存', this.value(event, 'path', ''))
    },

    onStateChange(event) {
      console.log('摄像头状态', event.detail || event)
    },

    onError(event) {
      const code = this.value(event, 'code', 'UNKNOWN')
      const message = this.value(event, 'message', '未知错误')
      console.error(code, message)
      uni.showToast({ title: message, icon: 'none' })
    }
  },

  onHide() {
    if (this.$refs.uvc) {
      this.$refs.uvc.close()
    }
  },

  onUnload() {
    if (this.$refs.uvc) {
      this.$refs.uvc.release()
    }
  }
}
</script>

<style>
.page {
  flex: 1;
  background-color: #111111;
}

.preview {
  width: 750rpx;
  height: 562rpx;
  background-color: #000000;
}
</style>

2. 刷新并打开设备

this.$refs.uvc.refreshDevices()

// deviceschange 返回设备列表后,传入 deviceId。
this.$refs.uvc.open(Number(this.devices[0].deviceId))

auto-starttrue 时,组件会尝试自动启动相关流程;仍建议监听 deviceschangeusbpermissionopenederror 事件,以便向用户展示准确状态。

组件属性

属性 类型 示例值 说明
auto-start Boolean true 组件初始化后是否自动开始设备发现或打开流程。
auto-request-permission Boolean true 是否自动申请所需权限。
preview-width Number 2592 期望的预览宽度。最终值以摄像头实际协商结果为准。
preview-height Number 1944 期望的预览高度。最终值以摄像头实际协商结果为准。
preview-fps Number 30 期望预览帧率。最终值取决于设备、格式和带宽。
format String mjpeg 视频格式,可选 mjpegyuyv
bandwidth-quirk Boolean true 是否启用 USB 带宽兼容处理,部分设备需要开启。
rotation Number 90 画面旋转角度,可选 090180270
mirror Boolean false 是否水平镜像预览画面。

预览参数是期望值,不保证摄像头一定支持。打开成功后,应以 opened 事件中的 size 为实际协商结果。

组件方法

通过组件引用调用方法:

const camera = this.$refs.uvc

refreshDevices()

重新扫描已连接的 UVC USB 摄像头。

camera.refreshDevices()

扫描结果通过 deviceschange 事件返回。

open(deviceId)

打开指定 USB 摄像头。

camera.open(Number(deviceId))
参数 类型 说明
deviceId Number deviceschange 事件返回的设备 ID。

成功后触发 opened,失败时触发 error

close()

关闭当前摄像头和预览。

camera.close()

关闭完成后触发 closed

release()

释放组件持有的原生资源。建议在页面 onUnload 中调用。

camera.release()

组件释放后,如需继续使用,应重新进入页面或重新创建组件实例。

takePhotoAuto()

拍摄照片并自动保存。

camera.takePhotoAuto()

拍照结果统一通过 photosaved 事件返回,不要将 JavaScript 回调函数作为参数跨 UTS 原生组件边界传递。

startRecordingAuto(withAudio)

开始录像并自动创建保存路径。

camera.startRecordingAuto(false)
参数 类型 说明
withAudio Boolean true 表示录制麦克风声音,false 表示仅录制视频。

使用声音时可能触发录音权限申请,并通过 audiopermission 返回结果。

stopRecording()

停止当前录像。

camera.stopRecording()

保存完成后触发 recordingsaved

setTransform(rotation, mirror)

设置预览画面的旋转和镜像。

camera.setTransform(90, true)
参数 类型 说明
rotation Number 旋转角度。
mirror Boolean 是否水平镜像。

changeFormat(format)

切换摄像头数据格式。

camera.changeFormat('mjpeg')

支持:

  • mjpeg
  • yuyv

格式切换后,设备支持的分辨率列表可能发生变化。应等待新的 sizeschange 事件,再调用 switchSize()

switchSize(width, height)

切换预览分辨率。

camera.switchSize(1920, 1080)

建议只传入 sizeschange 事件返回的受支持尺寸。

getState()

读取组件当前状态。

camera.getState()

状态通过 statechange 事件异步返回,通常包含:

{
  opened: true,
  recording: false,
  format: 'mjpeg',
  rotation: 90,
  mirror: false
}

openPermissionSettings()

打开应用权限设置页面,便于用户手动开启被拒绝的权限。

camera.openPermissionSettings()

组件事件

所有事件都建议使用兼容方法读取数据。不同运行环境中,事件数据可能表现为普通对象、event.detail 或带 get() 方法的 Map 对象。

log

原生日志事件。

{
  message: '日志内容'
}

deviceschange

USB 摄像头设备列表发生变化或扫描完成。

{
  devices: [
    {
      deviceId: 1,
      productName: 'USB Camera',
      deviceName: '/dev/bus/usb/...'
    }
  ]
}

常用字段:

字段 说明
deviceId 打开设备时使用的 ID。
productName USB 产品名称,部分设备可能为空。
deviceName 系统设备名称或路径。

usbpermission

USB 设备访问授权结果。

{
  granted: true
}

sizeschange

当前设备和视频格式支持的分辨率列表发生变化。

{
  sizes: [
    {
      width: 1920,
      height: 1080,
      fps: 30
    }
  ]
}

opened

摄像头打开并完成参数协商。

{
  device: {
    deviceId: 1,
    productName: 'USB Camera'
  },
  size: {
    width: 1920,
    height: 1080,
    fps: 30
  }
}

closed

摄像头已关闭。该事件通常不需要读取参数。

photosaved

照片已保存。

{
  path: '/storage/emulated/0/.../photo.jpg'
}

预览本地照片:

uni.previewImage({
  urls: [`file://${path}`]
})

recordingstart

录像已开始。

{
  path: '/storage/emulated/0/.../video.mp4'
}

recordingsaved

录像停止并保存完成。

{
  path: '/storage/emulated/0/.../video.mp4'
}

camerapermission

相机运行时权限结果。

{
  granted: true
}

audiopermission

麦克风运行时权限结果,仅在需要录制声音时使用。

{
  granted: true
}

statechange

getState() 的异步返回事件。

{
  opened: true,
  recording: false,
  format: 'mjpeg',
  rotation: 90,
  mirror: false
}

error

组件执行失败时触发。

{
  code: 'ERROR_CODE',
  message: '错误说明'
}

建议始终监听该事件并向用户展示 message

完整调用流程

推荐按照以下流程使用插件:

  1. 页面加载并创建 <uvc-camera>
  2. 调用 refreshDevices(),或启用 auto-start
  3. deviceschange 中取得设备列表。
  4. 调用 open(deviceId)
  5. 处理 USB、相机权限事件。
  6. opened 中读取实际协商的分辨率和帧率。
  7. 使用拍照、录像、格式切换、旋转或镜像功能。
  8. 页面隐藏时调用 close()
  9. 页面销毁时调用 release()

格式与分辨率

MJPEG

优点:

  • 相同 USB 带宽下通常可支持更高分辨率或更高帧率
  • 多数常见 UVC 摄像头支持

注意事项:

  • 需要设备提供 MJPEG 输出
  • 实际性能取决于设备解码能力

YUYV

优点:

  • 未压缩原始图像格式
  • 画面处理链路较直接

注意事项:

  • USB 带宽占用较高
  • 高分辨率下可能只能使用较低帧率

切换格式后,必须重新关注 sizeschange 返回的分辨率列表,因为 MJPEG 和 YUYV 支持的尺寸与帧率通常不同。

权限说明

插件可能涉及三类权限:

权限 使用场景
USB 设备授权 打开外接 UVC 摄像头。
相机权限 原生相机相关能力或系统要求。
麦克风权限 startRecordingAuto(true) 录制声音。

权限被拒绝后,可调用:

this.$refs.uvc.openPermissionSettings()

USB 设备授权通常与当前连接的 USB 设备相关。重新插拔设备或更换设备后,系统可能再次弹出授权提示。

生命周期建议

为了避免摄像头被占用、页面退出后仍在工作或原生资源泄漏,建议至少处理以下生命周期:

onHide() {
  if (this.$refs.uvc) {
    this.$refs.uvc.close()
  }
},

onUnload() {
  if (this.$refs.uvc) {
    this.$refs.uvc.release()
  }
}

录像过程中退出页面前,建议先调用 stopRecording(),等待 recordingsaved 后再关闭摄像头。

常见问题

1. 扫描不到 USB 摄像头

请检查:

  • 手机或平板是否支持 USB Host/OTG
  • OTG 功能是否已开启
  • 摄像头是否符合 UVC 标准
  • 转接线是否支持数据传输
  • 摄像头供电是否充足
  • 是否已调用 refreshDevices()
  • 是否在 Android 真机和自定义基座中运行

2. 已发现设备,但无法打开

请检查 usbpermissionerror 事件。常见原因包括 USB 授权被拒绝、设备被其他应用占用、摄像头输出格式不兼容或 USB 带宽不足。

3. 请求的分辨率没有生效

preview-widthpreview-heightpreview-fps 是期望参数。组件会根据设备能力进行协商,实际结果应以 opened 事件中的 size 为准。

需要精确切换时,请先读取 sizeschange,再使用其中的宽高调用 switchSize()

4. 切换 MJPEG/YUYV 后画面异常

切换格式后,应等待新的 sizeschange 事件,并选择该格式支持的分辨率。高分辨率 YUYV 可能因 USB 带宽不足而无法稳定预览。

5. 拍照方法调用后没有立即返回路径

takePhotoAuto() 是异步操作,文件路径通过 photosaved 事件返回。不要依赖方法返回值,也不要向原生组件方法传入 JavaScript 回调函数。

6. 本地照片无法预览

原生路径通常需要增加 file://

uni.previewImage({
  urls: [`file://${path}`]
})

同时确认文件仍然存在,并且应用具有对应文件访问能力。

7. 录像没有声音

开始录像时需传入 true

this.$refs.uvc.startRecordingAuto(true)

并确认 audiopermission 返回 granted: true

8. 页面退出后摄像头仍被占用

onHide 中调用 close(),并在 onUnload 中调用 release()。不要只隐藏组件而不释放原生摄像头资源。

调试建议

开发阶段建议监听全部状态事件:

<uvc-camera
  @log="onLog"
  @deviceschange="onDevicesChange"
  @usbpermission="onUsbPermission"
  @sizeschange="onSizesChange"
  @opened="onOpened"
  @closed="onClosed"
  @photosaved="onPhotoSaved"
  @recordingstart="onRecordingStart"
  @recordingsaved="onRecordingSaved"
  @camerapermission="onCameraPermission"
  @audiopermission="onAudioPermission"
  @statechange="onStateChange"
  @error="onError"
/>

当设备兼容性存在问题时,请记录以下信息:

  • 手机或平板型号及 Android 版本
  • 摄像头品牌与型号
  • USB 转接方式及供电情况
  • 当前格式:MJPEG 或 YUYV
  • 请求与实际协商的分辨率、帧率
  • logerror 事件内容

注意事项

  • 仅使用摄像头实际支持的格式和分辨率。
  • 不同 UVC 摄像头的能力差异较大,建议在目标设备上充分测试。
  • 录像未保存完成前不要立即释放组件。
  • 使用带声音录像时,应向用户明确说明麦克风用途。
  • 应用进入后台或页面离开时,应及时关闭摄像头。
  • bandwidth-quirk 是否需要开启取决于摄像头及设备兼容性。

API 速查

const camera = this.$refs.uvc

camera.refreshDevices()
camera.open(deviceId)
camera.close()
camera.release()

camera.takePhotoAuto()
camera.startRecordingAuto(false)
camera.stopRecording()

camera.setTransform(90, false)
camera.changeFormat('mjpeg')
camera.switchSize(1920, 1080)

camera.getState()
camera.openPermissionSettings()

License

隐私、权限声明

1. 本插件需要申请的系统权限列表:

需要访问USB设备;拍照、录像功能可能需要相机、麦克风及媒体文件访问权限。

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不主动采集或上传用户数据。

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。