更新记录
1.1.4(2026-05-26)
修复小米机型拍照后被旋转问题
1.1.3(2026-04-27)
安卓补上 onCaptureStarted() 方法
1.1.2(2026-04-22)
更新安卓,对其IOS
查看更多平台兼容性
uni-app(4.81)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | - | - | × | √ | 6.0 | 12 | - |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | - | - | - | - | - | - |
rd-camera 自定义相机插件
高性能自定义相机 UTS 插件,基于 CameraX(Android)和 AVFoundation(iOS)实现。 尽量使用 HBuilder X -alpha 版本
功能特性
- 自定义相机预览(支持缩放模式配置)
- 拍照功能(4 档质量配置,前置自动镜像,智能裁剪对齐预览)
- 视频录制(支持时长限制、音频录制、4 档质量)
- 前后摄像头切换
- 闪光灯控制(开关切换 + 模式设置)
- 音频处理:回声消除、噪声抑制、自动增益控制
- 输出文件格式选择(iOS: mov/mp4, Android: mp4)
- 线程安全的状态管理(ReentrantLock + AtomicBoolean + @Volatile)
- 页面卸载时自动释放资源,避免内存泄漏
安装
将 uni_modules/rd-camera 目录复制到项目的 uni_modules 目录下。
平台支持
| 平台 | 支持情况 |
|---|---|
| Android | √(minSDK 23,基于 CameraX) |
| iOS | √(基于 AVFoundation) |
| 小程序 | - |
| Web | - |
仅支持 nvue 页面使用
权限要求
Android
android.permission.CAMERA- 相机权限android.permission.RECORD_AUDIO- 麦克风权限android.permission.WRITE_EXTERNAL_STORAGE- 存储权限(Android 9 及以下)
iOS
在 Info.plist 中添加以下权限描述:
<key>NSCameraUsageDescription</key>
<string>需要访问相机进行拍照和录制视频</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要访问麦克风录制视频声音</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>需要访问相册保存照片和视频</string>
API
属性 (Props)
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
cameraPosition |
String | "back" |
初始摄像头位置,"front" 前置 / "back" 后置 |
scaleType |
Number | 3 |
预览缩放模式,详见下方说明 |
scaleType 缩放模式
| 值 | 常量 | 说明 |
|---|---|---|
| 0 | FIT_CENTER | 保持比例,居中显示,可能有黑边 |
| 1 | FIT_START | 保持比例,靠上显示 |
| 2 | FIT_END | 保持比例,靠下显示 |
| 3 | FILL_CENTER | 填充视图,居中裁剪,无黑边(默认) |
| 4 | FILL_START | 填充视图,靠上裁剪 |
| 5 | FILL_END | 填充视图,靠下裁剪 |
scaleType仅 Android 平台生效,iOS 预览始终为填充模式。
事件 (Events)
| 事件名 | 说明 | 回调参数 |
|---|---|---|
onCameraReady |
相机初始化完成 | { detail: { message } } |
onPhotoCaptured |
拍照完成 | { detail: { success, message, path } } |
onRecordingFinished |
录制完成 | { detail: { success, message, path } } |
onRecordingDurationUpdate |
录制时长更新(每秒) | { detail: { duration } } |
onError |
错误回调 | { detail: { message } } |
方法 (Methods)
| 方法名 | 参数 | 返回值 | 说明 |
|---|---|---|---|
capturePhoto(saveToAlbum, quality) |
saveToAlbum: boolean, quality: string | - | 拍照 |
startRecording(saveToAlbum, quality, recordAudio, maxDuration, echoCancellation, noiseSuppression, autoGainControl, outputFileType) |
见下方说明 | - | 开始录制 |
stopRecording() |
- | - | 停止录制 |
getRecordingStatus() |
- | boolean | 获取录制状态 |
switchCamera() |
- | - | 切换前后摄像头 |
toggleFlash() |
- | boolean | 切换闪光灯,返回是否成功 |
setFlashMode(mode) |
mode: "on" | "off" | - | 设置闪光灯模式 |
startCamera() |
- | - | 启动相机预览 |
stopCamera() |
- | - | 停止相机预览 |
initCameraPosition(position) |
position: "front" | "back" | - | 动态设置摄像头位置 |
changeScaleType(scaleType) |
scaleType: number (0-5) | - | 动态更改预览缩放模式 |
prepareForUnload() |
- | - | 准备卸载,释放资源 |
参数说明
quality(质量)
| 值 | 拍照说明 | 视频说明 |
|---|---|---|
"low" |
压缩率 30%,降采样至 1080p 以内 | SD 480p |
"medium" |
压缩率 60%,降采样至 1080p 以内 | HD 720p |
"high" |
压缩率 80%,降采样至 1080p 以内(推荐) | FHD 1080p |
"photo" |
无压缩,原始分辨率 | UHD 2160p |
startRecording 参数
| 参数 | 类型 | 说明 |
|---|---|---|
| saveToAlbum | boolean | 是否保存到相册 |
| quality | string | 视频质量 |
| recordAudio | boolean | 是否录制音频 |
| maxDuration | number | 最大时长(秒),0 表示不限制 |
| echoCancellation | boolean | 回声消除(Android 自动处理,参数保留兼容 iOS) |
| noiseSuppression | boolean | 噪声抑制(Android 自动处理,参数保留兼容 iOS) |
| autoGainControl | boolean | 自动增益控制(Android 自动处理,参数保留兼容 iOS) |
| outputFileType | string | 输出文件格式:"mp4"(Android 默认)/ "mov"(iOS 默认)/ "mp4"(iOS 异步转换) |
注意:
outputFileType在 Android 平台始终输出.mp4,该参数主要用于 iOS 平台选择输出格式。
事件回调数据
onPhotoCaptured / onRecordingFinished
{
detail: {
success: boolean, // 是否成功
message: string, // 提示信息
path: string // 文件路径(成功时)
}
}
onCameraReady / onError
{
detail: {
message: string // 提示信息
}
}
onRecordingDurationUpdate
{
detail: {
duration: number // 当前录制时长(秒,浮点数)
}
}
使用示例
Vue2 使用示例
<template>
<view class="container">
<!-- 相机组件 -->
<rd-camera
ref="camera"
class="camera-preview"
:cameraPosition="cameraPosition"
:scaleType="3"
@onCameraReady="onCameraReady"
@onPhotoCaptured="onPhotoCaptured"
@onRecordingFinished="onRecordingFinished"
@onRecordingDurationUpdate="onRecordingDurationUpdate"
@onError="onError"
/>
<!-- 控制按钮 -->
<view class="controls">
<button @click="capturePhoto">拍照</button>
<button @click="startRecording">开始录制</button>
<button @click="stopRecording">停止录制</button>
<button @click="switchCamera">切换摄像头</button>
<button @click="toggleFlash">闪光灯</button>
</view>
<!-- 录制时长显示 -->
<text v-if="isRecording">录制中: {{ recordingDuration }}秒</text>
</view>
</template>
<script>
export default {
data() {
return {
isRecording: false,
recordingDuration: 0,
isCameraReady: false,
cameraPosition: 'back'
}
},
methods: {
onCameraReady(e) {
console.log('相机已就绪', e.detail.message)
this.isCameraReady = true
},
capturePhoto() {
if (!this.isCameraReady) {
uni.showToast({ title: '相机未就绪', icon: 'none' })
return
}
this.$refs.camera.capturePhoto(true, 'high')
},
onPhotoCaptured(e) {
const { success, message, path } = e.detail
if (success) {
console.log('拍照成功,路径:', path)
uni.showToast({ title: '拍照成功', icon: 'success' })
} else {
uni.showToast({ title: message, icon: 'none' })
}
},
startRecording() {
if (!this.isCameraReady) {
uni.showToast({ title: '相机未就绪', icon: 'none' })
return
}
if (this.$refs.camera.getRecordingStatus()) {
uni.showToast({ title: '正在录制中', icon: 'none' })
return
}
this.recordingDuration = 0
this.$refs.camera.startRecording(
true, // saveToAlbum
'high', // quality
true, // recordAudio
60, // maxDuration (60秒)
true, // echoCancellation
true, // noiseSuppression
true, // autoGainControl
'mp4' // outputFileType
)
this.isRecording = true
},
stopRecording() {
this.$refs.camera.stopRecording()
this.isRecording = false
},
onRecordingFinished(e) {
const { success, message, path } = e.detail
this.isRecording = false
if (success) {
console.log('录制成功,路径:', path)
uni.showToast({ title: '录制成功', icon: 'success' })
} else {
uni.showToast({ title: message, icon: 'none' })
}
},
onRecordingDurationUpdate(e) {
this.recordingDuration = Math.floor(e.detail.duration)
},
switchCamera() {
this.$refs.camera.switchCamera()
this.cameraPosition = this.cameraPosition === 'front' ? 'back' : 'front'
},
toggleFlash() {
const result = this.$refs.camera.toggleFlash()
uni.showToast({
title: result ? '闪光灯已开启' : '闪光灯已关闭',
icon: 'none'
})
},
onError(e) {
console.error('相机错误:', e.detail.message)
uni.showToast({ title: e.detail.message, icon: 'none' })
}
},
beforeUnmount() {
if (this.isRecording) {
this.$refs.camera.stopRecording()
}
}
}
</script>
<style>
.container {
flex: 1;
}
.camera-preview {
width: 100%;
height: 500rpx;
}
.controls {
display: flex;
flex-wrap: wrap;
justify-content: center;
padding: 20rpx;
}
.controls button {
margin: 10rpx;
font-size: 28rpx;
}
</style>
Vue3 使用示例
<template>
<view class="container">
<rd-camera
ref="cameraRef"
class="camera-preview"
:cameraPosition="cameraPosition"
:scaleType="3"
@onCameraReady="onCameraReady"
@onPhotoCaptured="onPhotoCaptured"
@onRecordingFinished="onRecordingFinished"
@onRecordingDurationUpdate="onRecordingDurationUpdate"
@onError="onError"
/>
<view class="controls">
<button @click="capturePhoto">拍照</button>
<button @click="handleStartRecording">
{{ isRecording ? '录制中...' : '开始录制' }}
</button>
<button @click="handleStopRecording" :disabled="!isRecording">停止录制</button>
<button @click="switchCamera">切换摄像头</button>
<button @click="toggleFlash">闪光灯</button>
</view>
<text v-if="isRecording" class="duration-text">
录制中: {{ recordingDuration }}秒
</text>
<image v-if="lastPhotoPath" :src="lastPhotoPath" class="preview-image" mode="aspectFit" />
</view>
</template>
<script setup>
import { ref } from 'vue'
const cameraRef = ref(null)
const isRecording = ref(false)
const recordingDuration = ref(0)
const isCameraReady = ref(false)
const lastPhotoPath = ref('')
const cameraPosition = ref('back')
const onCameraReady = (e) => {
console.log('相机已就绪', e.detail.message)
isCameraReady.value = true
}
const capturePhoto = () => {
if (!isCameraReady.value) {
uni.showToast({ title: '相机未就绪', icon: 'none' })
return
}
cameraRef.value.capturePhoto(true, 'high')
}
const onPhotoCaptured = (e) => {
const { success, message, path } = e.detail
if (success) {
lastPhotoPath.value = path
uni.showToast({ title: '拍照成功', icon: 'success' })
} else {
uni.showToast({ title: message, icon: 'none' })
}
}
const handleStartRecording = () => {
if (!isCameraReady.value) {
uni.showToast({ title: '相机未就绪', icon: 'none' })
return
}
if (cameraRef.value.getRecordingStatus()) {
uni.showToast({ title: '正在录制中', icon: 'none' })
return
}
recordingDuration.value = 0
cameraRef.value.startRecording(
true, // saveToAlbum
'high', // quality
true, // recordAudio
60, // maxDuration
true, // echoCancellation
true, // noiseSuppression
true, // autoGainControl
'mp4' // outputFileType
)
isRecording.value = true
}
const handleStopRecording = () => {
cameraRef.value.stopRecording()
}
const onRecordingFinished = (e) => {
const { success, message, path } = e.detail
isRecording.value = false
if (success) {
console.log('录制成功,路径:', path)
uni.showToast({ title: '录制成功', icon: 'success' })
} else {
uni.showToast({ title: message, icon: 'none' })
}
}
const onRecordingDurationUpdate = (e) => {
recordingDuration.value = Math.floor(e.detail.duration)
}
const switchCamera = () => {
cameraRef.value.switchCamera()
cameraPosition.value = cameraPosition.value === 'front' ? 'back' : 'front'
}
const toggleFlash = () => {
const result = cameraRef.value.toggleFlash()
uni.showToast({
title: result ? '闪光灯已开启' : '闪光灯已关闭',
icon: 'none'
})
}
const onError = (e) => {
console.error('相机错误:', e.detail.message)
uni.showToast({ title: e.detail.message, icon: 'none' })
}
</script>
<style scoped>
.container {
flex: 1;
background-color: #000;
}
.camera-preview {
width: 100%;
height: 500rpx;
}
.controls {
display: flex;
flex-wrap: wrap;
justify-content: center;
padding: 20rpx;
background-color: #fff;
}
.controls button {
margin: 10rpx;
font-size: 28rpx;
padding: 15rpx 30rpx;
}
.duration-text {
color: #fff;
text-align: center;
padding: 20rpx;
font-size: 32rpx;
}
.preview-image {
width: 100%;
height: 300rpx;
margin-top: 20rpx;
}
</style>
平台差异
| 功能 | Android | iOS |
|---|---|---|
| 相机框架 | CameraX | AVFoundation |
| 预览缩放模式(scaleType) | 支持 6 种模式 | 始终填充模式 |
| 输出文件格式 | 固定 .mp4 |
.mov(默认)或 .mp4(异步转换) |
| 回声消除/噪声抑制/自动增益 | 系统自动处理,参数保留兼容 | voiceChat 模式自动启用 |
| 拍照前置镜像 | 自动镜像处理 | 自动镜像处理 |
| 拍照裁剪 | 根据预览层宽高比智能裁剪 | 根据预览层宽高比智能裁剪 |
| 保存到相册 | MediaStore(Android 10+)/ MediaScanner | Photos 框架 |
注意事项
- 仅支持 nvue 页面:本组件为原生组件,只能在 nvue 页面中使用。
- 页面卸载:在页面卸载前(
beforeUnmount/onBeforeUnmount)建议手动调用stopRecording()停止录制,组件会在NVBeforeUnload中自动释放资源。 - 权限检查:组件在 Android 端会自动请求相机和麦克风权限;iOS 端需要在
Info.plist中手动配置权限描述。 - maxDuration 参数:传入
0表示不限制录制时长;设置最大时长后,到达时间会自动停止录制并触发onRecordingFinished事件。 - 录制状态同步:
isRecording状态应由onRecordingFinished事件驱动更新,而非仅依赖前端标记,以确保与原生状态一致。 - saveToAlbum:Android 端录制始终先写入缓存目录,录制完成后如需保存到相册再拷贝到 MediaStore,避免权限问题导致静默失败。

收藏人数:
购买普通授权版(
试用
赞赏(0)
下载 73
赞赏 0
下载 12096050
赞赏 1918
赞赏
京公网安备:11010802035340号