更新记录

1.0.0(2026-08-18)

新版本发布,后续增加推流


平台兼容性

uni-app(4.0)

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

YZJ UVC Camera

仅适用于 App-Android 的 nvue 原生组件,用于标准 USB UVC 摄像头的预览、拍照和 MP4 录制。录音来源可选择 UVC 自带的 UAC 麦克风或手机麦克风。

前置条件

  • 仅支持 Android API 21 及以上,且必须在 nvue 页面使用。
  • 应用需要授予 android.permission.CAMERA 权限,并先调用 requestUsbPermission 获取 USB 授权。
  • 使用手机麦克风时还需要授予 android.permission.RECORD_AUDIO 权限。
  • 组件仅识别 isUvc === true 的设备。

录音来源

默认使用 UVC 摄像头自带的 UAC 麦克风。若设备不带麦克风或录制无声,可在关闭预览后切换到手机麦克风:

this.$refs.uvcCam.setAudioSource('system')

切换后点击“申请授权”,授予 CAMERARECORD_AUDIO 权限,再重新打开预览并开始录像。切回 UVC 麦克风:

this.$refs.uvcCam.setAudioSource('device')

音频来源不能在预览已打开、录制中或正在启动录制时切换。

完整示例

<template>
  <view class="page">
    <yzj-uvc-cam
      ref="uvcCam"
      class="preview"
      @statechange="onUvcEvent('statechange', $event)"
      @devicechange="onUvcEvent('devicechange', $event)"
      @resolutionschange="onUvcEvent('resolutionschange', $event)"
      @error="onUvcEvent('error', $event)"
      @photo="onUvcEvent('photo', $event)"
      @recordchange="onUvcEvent('recordchange', $event)"
    />
    <text class="status">{{ statusText }}</text>
    <radio-group class="row" @change="changeAudioSource">
      <label><radio value="device" :checked="audioSource === 'device'" :disabled="previewing" /><text>UVC 麦克风</text></label>
      <label><radio value="system" :checked="audioSource === 'system'" :disabled="previewing" /><text>手机麦克风</text></label>
    </radio-group>
    <view class="row">
      <button @click="refreshDevices">刷新设备</button>
      <button @click="requestPermission">申请授权</button>
    </view>
    <view class="row">
      <button @click="openUvcCamera">打开预览</button>
      <button @click="closeUvcCamera">关闭预览</button>
    </view>
    <view class="row">
      <button @click="testPreview">测试是否预览</button>
      <button @click="readResolutions">读取分辨率</button>
    </view>
    <view class="row">
      <button @click="switchResolution">切换分辨率</button>
      <button @click="takePhoto">拍照</button>
    </view>
    <button @click="toggleRecording">{{ recording ? '停止录像' : '开始录像' }}</button>
    <text class="detail">支持分辨率:{{ JSON.stringify(resolutions) }}</text>
    <text class="detail">照片:{{ photoPath }}</text>
    <text class="detail">录像:{{ videoPath }}</text>
  </view>
</template>

<script>
export default {
  data() {
    return {
      deviceId: '',
      width: 1280,
      height: 720,
      audioSource: 'device',
      previewing: false,
      recording: false,
      resolutions: [],
      photoPath: '',
      videoPath: '',
      statusText: '等待 UVC 摄像头接入'
    }
  },
  onReady() {
    setTimeout(() => this.refreshDevices(), 100)
  },
  onUnload() {
    const camera = this.$refs.uvcCam
    if (camera) camera.release()
  },
  methods: {
    getCamera() {
      return this.$refs.uvcCam
    },
    decodeEvent(event) {
      const raw = event && event.detail != null ? event.detail : event
      const value = raw && typeof raw === 'object' && raw.value != null ? raw.value : raw
      return typeof value === 'string' ? JSON.parse(value) : (value || {})
    },
    refreshDevices() {
      const devices = JSON.parse(this.getCamera().getDevices())
      const camera = devices.find((item) => item && item.isUvc === true)
      this.deviceId = camera ? String(camera.deviceId) : ''
      this.statusText = camera ? '已找到 UVC 摄像头' : '未找到 UVC 摄像头'
    },
    requestPermission() {
      if (!this.deviceId) return
      const permissions = ['android.permission.CAMERA']
      if (this.audioSource === 'system') permissions.push('android.permission.RECORD_AUDIO')
      plus.android.requestPermissions(permissions, (result) => {
        const granted = Array.isArray(result.granted) ? result.granted : []
        const cameraGranted = granted.indexOf('android.permission.CAMERA') >= 0
        const audioGranted = this.audioSource !== 'system' || granted.indexOf('android.permission.RECORD_AUDIO') >= 0
        if (cameraGranted && audioGranted) {
          this.getCamera().requestUsbPermission(this.deviceId)
        } else {
          this.statusText = '所需权限未全部授予'
        }
      })
    },
    changeAudioSource(event) {
      if (this.previewing || this.recording) return
      this.audioSource = String(event.detail.value)
      this.getCamera().setAudioSource(this.audioSource)
    },
    openUvcCamera() {
      if (this.deviceId) {
        this.getCamera().setAudioSource(this.audioSource)
        this.getCamera().openCamera(this.deviceId, this.width, this.height)
      }
    },
    closeUvcCamera() {
      if (this.recording) return
      this.getCamera().closeCamera()
    },
    testPreview() {
      this.statusText = this.previewing ? '预览已打开' : '预览未打开'
    },
    readResolutions() {
      this.getCamera().getSupportedResolutions()
    },
    switchResolution() {
      if (this.recording || this.resolutions.length === 0) return
      const currentIndex = this.resolutions.findIndex((item) => item.width === this.width && item.height === this.height)
      const next = this.resolutions[(currentIndex + 1) % this.resolutions.length]
      this.width = Number(next.width)
      this.height = Number(next.height)
      this.getCamera().setResolution(this.width, this.height)
    },
    takePhoto() {
      if (this.previewing) this.getCamera().takePhoto('')
    },
    toggleRecording() {
      if (this.recording) {
        this.getCamera().stopRecording()
      } else if (this.previewing) {
        this.getCamera().startRecording('')
      }
    },
    onUvcEvent(type, event) {
      const payload = this.decodeEvent(event)
      if (type === 'statechange') {
        if (payload.state === 'PREVIEWING') this.previewing = true
        if (payload.state === 'CLOSED') {
          this.previewing = false
          this.recording = false
        }
        this.statusText = payload.state || '状态变化'
      }
      if (type === 'devicechange' && payload.action === 'attached' && payload.device && payload.device.isUvc === true) {
        this.deviceId = String(payload.device.deviceId)
      }
      if (type === 'devicechange' && payload.action === 'detached') {
        this.deviceId = ''
        this.previewing = false
        this.recording = false
      }
      if (type === 'resolutionschange') {
        this.resolutions = Array.isArray(payload.resolutions) ? payload.resolutions : []
      }
      if (type === 'photo') this.photoPath = payload.path || ''
      if (type === 'recordchange') {
        this.recording = payload.state === 'started'
        if (payload.state === 'stopped') this.videoPath = payload.path || ''
      }
      if (type === 'error') this.statusText = `${payload.code}: ${payload.message}`
    }
  }
}
</script>

<style>
.page { flex: 1; }
.preview { width: 750rpx; height: 422rpx; background-color: #000000; }
.row { flex-direction: row; }
.status { padding: 16rpx; }
.detail { padding: 8rpx 16rpx; }
</style>

USB 授权成功后会触发 statechangeUSB_PERMISSION_GRANTED 状态,再调用 openCamera。首次使用时,应在页面层同时申请 Android 相机权限。

API

方法 参数 说明
getDevices() 返回 USB 设备 JSON 数组。仅对 isUvc === true 的设备调用后续方法。
requestUsbPermission(deviceId) deviceId: string 请求指定 UVC 设备的 USB 授权。
openCamera(deviceId, width, height) deviceId: stringwidth: numberheight: number 使用指定分辨率打开预览。调用前必须完成 USB 授权。已打开相同设备和分辨率时仅恢复预览状态。
closeCamera() 停止预览并释放当前相机连接。录制中应先调用 stopRecording()
getSupportedResolutions() 返回当前设备支持分辨率的 JSON 数组,并触发 resolutionschange
isPreviewing() 原生同步查询接口。nvue 组件方法的同步返回值可能无法稳定传回页面,页面应优先根据 statechangePREVIEWINGCLOSED 维护状态。
setAudioSource(source) source: string 设置录音来源:device 使用 UVC 自带麦克风,system 使用手机麦克风。必须在关闭预览后调用。
setResolution(width, height) width: numberheight: number 切换预览分辨率。仅可传入 getSupportedResolutions() 返回的分辨率,录制中不可调用。
takePhoto(outputPath) outputPath: string 保存当前预览画面为 JPEG。传入空字符串时保存到应用图片目录。
startRecording(outputPath) outputPath: string 开始 MP4 录制,使用 setAudioSource 选定的音频来源。传入空字符串时保存到应用视频目录。
stopRecording() 停止录制,成功后通过 recordchange 返回文件路径。
release() 释放组件资源。组件卸载时会自动调用。

事件

所有事件负载均为 JSON 字符串。

事件 关键字段 说明
statechange stateoperation 相机状态。常用状态:USB_PERMISSION_GRANTEDPREVIEWINGRESOLUTION_CHANGINGRESOLUTION_CHANGEDCLOSED
devicechange actiondevice USB 设备接入或拔出,actionattacheddetached
resolutionschange resolutions 支持分辨率数组,元素为 { width, height }
photo pathsize 照片保存成功。
recordchange statepathsizereason 录制状态:startedstoppedaborted
error codemessageoperation 操作失败原因。

错误码

错误码 说明
USB_DEVICE_NOT_FOUND 未找到目标 USB 设备。
USB_DEVICE_NOT_UVC 目标设备不是 UVC 摄像头。
USB_PERMISSION_REQUIRED 尚未获取 USB 授权。
USB_PERMISSION_DENIED USB 授权被拒绝或授权请求失败。
USB_DEVICE_DISCONNECTED UVC 摄像头已拔出。
UVC_OPEN_FAILED 打开或恢复 UVC 预览失败。
RESOLUTION_INVALID 分辨率参数无效。
RESOLUTION_UNSUPPORTED 设备不支持该分辨率。
CAMERA_NOT_OPEN 摄像头尚未打开。
CAMERA_BUSY 录制或分辨率切换期间不允许当前操作。
AUDIO_SOURCE_INVALID 音频来源不是 devicesystem
PHOTO_CAPTURE_FAILED 拍照或保存照片失败。
RECORD_NOT_STARTED 当前未录制。
RECORD_ALREADY_STARTED 已有录制任务进行中。
RECORD_WRITE_FAILED 录制文件写入或封装失败。
INTERNAL_ERROR 原生内部错误。

分辨率切换说明

切换分辨率后,等待 RESOLUTION_CHANGEDPREVIEWING 状态再执行拍照和录像。部分 UVC 设备在切换后画面已经更新,但原生预览状态不会自动恢复;此时再次调用一次 openCamera(deviceId, width, height) 即可恢复会话。

录制验证

  1. 关闭预览后选择录音来源。
  2. 点击“申请授权”,手机麦克风模式必须授权录音权限。
  3. 打开预览,开始录像并录制至少 3 秒。
  4. 停止录像后播放生成的 MP4,确认视频画面、音轨和音画同步正常。

限制

  • 代码按 AndroidUSBCamera 3.3.3 的 API 实现,并在 utssdk/app-android/libs 内附带从源码构建的 libausbclibuvclibnative AAR,支持 armeabi-v7aarm64-v8a
  • 默认使用 UVC 设备自带的 UAC 麦克风;设备没有 UAC 音频接口时该模式录制无声视频,可在关闭预览后切换为手机麦克风。
  • 摄像头拔出时组件会停止预览和录制并释放资源,重插后须重新请求授权并打开。

隐私、权限声明

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

android.permission.CAMERA:打开 UVC 摄像头预览所需。 android.permission.RECORD_AUDIO:选择“手机麦克风”录制时所需。 USB Host:通过 <uses-feature android:name="android.hardware.usb.host" /> 声明,用于连接 OTG/UVC 设备。 USB 设备授权:运行时调用 requestUsbPermission(deviceId) 请求指定 UVC 设备访问权限。 android.permission.WRITE_EXTERNAL_STORAGE:仅 Android 9(API 28)及以下保存媒体文件时使用。

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

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

暂无用户评论。