更新记录
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')
切换后点击“申请授权”,授予 CAMERA 和 RECORD_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 授权成功后会触发 statechange 的 USB_PERMISSION_GRANTED 状态,再调用 openCamera。首次使用时,应在页面层同时申请 Android 相机权限。
API
| 方法 | 参数 | 说明 |
|---|---|---|
getDevices() |
无 | 返回 USB 设备 JSON 数组。仅对 isUvc === true 的设备调用后续方法。 |
requestUsbPermission(deviceId) |
deviceId: string |
请求指定 UVC 设备的 USB 授权。 |
openCamera(deviceId, width, height) |
deviceId: string、width: number、height: number |
使用指定分辨率打开预览。调用前必须完成 USB 授权。已打开相同设备和分辨率时仅恢复预览状态。 |
closeCamera() |
无 | 停止预览并释放当前相机连接。录制中应先调用 stopRecording()。 |
getSupportedResolutions() |
无 | 返回当前设备支持分辨率的 JSON 数组,并触发 resolutionschange。 |
isPreviewing() |
无 | 原生同步查询接口。nvue 组件方法的同步返回值可能无法稳定传回页面,页面应优先根据 statechange 的 PREVIEWING 和 CLOSED 维护状态。 |
setAudioSource(source) |
source: string |
设置录音来源:device 使用 UVC 自带麦克风,system 使用手机麦克风。必须在关闭预览后调用。 |
setResolution(width, height) |
width: number、height: number |
切换预览分辨率。仅可传入 getSupportedResolutions() 返回的分辨率,录制中不可调用。 |
takePhoto(outputPath) |
outputPath: string |
保存当前预览画面为 JPEG。传入空字符串时保存到应用图片目录。 |
startRecording(outputPath) |
outputPath: string |
开始 MP4 录制,使用 setAudioSource 选定的音频来源。传入空字符串时保存到应用视频目录。 |
stopRecording() |
无 | 停止录制,成功后通过 recordchange 返回文件路径。 |
release() |
无 | 释放组件资源。组件卸载时会自动调用。 |
事件
所有事件负载均为 JSON 字符串。
| 事件 | 关键字段 | 说明 |
|---|---|---|
statechange |
state、operation |
相机状态。常用状态:USB_PERMISSION_GRANTED、PREVIEWING、RESOLUTION_CHANGING、RESOLUTION_CHANGED、CLOSED。 |
devicechange |
action、device |
USB 设备接入或拔出,action 为 attached 或 detached。 |
resolutionschange |
resolutions |
支持分辨率数组,元素为 { width, height }。 |
photo |
path、size |
照片保存成功。 |
recordchange |
state、path、size、reason |
录制状态:started、stopped 或 aborted。 |
error |
code、message、operation |
操作失败原因。 |
错误码
| 错误码 | 说明 |
|---|---|
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 |
音频来源不是 device 或 system。 |
PHOTO_CAPTURE_FAILED |
拍照或保存照片失败。 |
RECORD_NOT_STARTED |
当前未录制。 |
RECORD_ALREADY_STARTED |
已有录制任务进行中。 |
RECORD_WRITE_FAILED |
录制文件写入或封装失败。 |
INTERNAL_ERROR |
原生内部错误。 |
分辨率切换说明
切换分辨率后,等待 RESOLUTION_CHANGED 或 PREVIEWING 状态再执行拍照和录像。部分 UVC 设备在切换后画面已经更新,但原生预览状态不会自动恢复;此时再次调用一次 openCamera(deviceId, width, height) 即可恢复会话。
录制验证
- 关闭预览后选择录音来源。
- 点击“申请授权”,手机麦克风模式必须授权录音权限。
- 打开预览,开始录像并录制至少 3 秒。
- 停止录像后播放生成的 MP4,确认视频画面、音轨和音画同步正常。
限制
- 代码按 AndroidUSBCamera 3.3.3 的 API 实现,并在
utssdk/app-android/libs内附带从源码构建的libausbc、libuvc、libnativeAAR,支持armeabi-v7a、arm64-v8a。 - 默认使用 UVC 设备自带的 UAC 麦克风;设备没有 UAC 音频接口时该模式录制无声视频,可在关闭预览后切换为手机麦克风。
- 摄像头拔出时组件会停止预览和录制并释放资源,重插后须重新请求授权并打开。

收藏人数:
购买源码授权版(
试用
赞赏(0)
下载 19
赞赏 0
下载 12513665
赞赏 1943
赞赏
京公网安备:11010802035340号