更新记录

1.1.5(2026-07-14)

优化在低端机型性能。 更新安卓最新版依赖。 注意设置质量参数改变了,不在方法里传了

1.1.4(2026-05-26)

修复小米机型拍照后被旋转问题

1.1.3(2026-04-27)

安卓补上 onCaptureStarted() 方法

查看更多

平台兼容性

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 版本

功能特性

  • 自定义相机预览(支持缩放模式配置)
  • 拍照功能(Android 支持 ZSL 快拍,4 档质量配置,前置自动镜像,智能裁剪对齐预览)
  • 视频录制(支持时长限制、音频录制、4 档质量)
  • 前后摄像头切换
  • 闪光灯控制(开关切换 + 模式设置)
  • 音频处理:回声消除、噪声抑制、自动增益控制
  • 输出文件格式选择(iOS: mov/mp4, Android: mp4)
  • 页面卸载时自动释放资源,避免内存泄漏

安装

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 预览缩放模式,详见下方说明
photoQuality String "high" Android/iOS 照片和视频共用质量配置

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 } }
onPhotoCaptureStarted Android 开始曝光,可用于即时快门反馈 { detail: { message } }
onPhotoCaptured 拍照完成 { detail: { success, message, path } }
onRecordingFinished 录制完成 { detail: { success, message, path } }
onRecordingDurationUpdate 录制时长更新(每秒) { detail: { duration } }
onError 错误回调 { detail: { message } }

Android 照片缓存文件完成后立即回调,相册复制在后台继续。视频在 Android 10+ 且 saveToAlbum=true 时由 CameraX 直接写入 MediaStore,完成事件的 pathcontent:// URI;不保存相册时返回缓存文件路径。

方法 (Methods)

方法名 参数 返回值 说明
capturePhoto(saveToAlbum) saveToAlbum: boolean - 使用 photoQuality 配置拍照
startRecording(saveToAlbum, recordAudio, maxDuration, echoCancellation, noiseSuppression, autoGainControl, outputFileType) 见下方说明 - 使用 photoQuality 配置开始录制
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() - - 准备卸载,释放资源

Android 与 iOS 使用相同的方法签名,质量统一通过 photoQuality 属性传入。

参数说明

photoQuality(照片/视频质量)

Android 拍照 iOS 拍照 视频说明
"low" 快速照片,JPEG 30% 速度优先 SD 480p
"medium" 快速照片,JPEG 60% 速度优先 HD 720p
"high" 快速照片,JPEG 80% 质量与速度平衡(推荐 FHD 1080p
"photo" 全尺寸照片,JPEG 100% 最大照片尺寸、质量优先 UHD 2160p

Android 与 iOS 的照片和视频都共用 photoQualitycapturePhotostartRecording 不再接收质量参数。Android 属性变化时会提前重建 CameraX 拍照会话并预热 ZSL。

startRecording 参数

参数 类型 说明
saveToAlbum boolean 是否保存到相册
recordAudio boolean 是否录制音频
maxDuration number 最大时长(秒),0 表示不限制
echoCancellation boolean 回声消除(Android 自动处理,参数保留兼容 iOS)
noiseSuppression boolean 噪声抑制(Android 自动处理,参数保留兼容 iOS)
autoGainControl boolean 自动增益控制(Android 自动处理,参数保留兼容 iOS)
outputFileType string 输出格式;Android 固定 .mp4,iOS 支持原有格式选择

事件回调数据

onPhotoCaptured / onRecordingFinished

{
  detail: {
    success: boolean,   // 是否成功
    message: string,    // 提示信息
    path: string        // 文件路径;Android 10+ 相册视频为 content:// URI
  }
}

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"
      photoQuality="high"
      @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)
    },

    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
        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"
      photoQuality="high"
      @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)
}

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
    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 框架

注意事项

  1. 仅支持 nvue 页面:本组件为原生组件,只能在 nvue 页面中使用。
  2. 页面卸载:在页面卸载前(beforeUnmount/onBeforeUnmount)建议手动调用 stopRecording() 停止录制,组件会在 NVBeforeUnload 中自动释放资源。
  3. 权限检查:组件在 Android 端会自动请求相机和麦克风权限;iOS 端需要在 Info.plist 中手动配置权限描述。
  4. maxDuration 参数:传入 0 表示不限制录制时长;设置最大时长后,到达时间会自动停止录制并触发 onRecordingFinished 事件。
  5. 录制状态同步isRecording 状态应由 onRecordingFinished 事件驱动更新,而非仅依赖前端标记,以确保与原生状态一致。
  6. saveToAlbum:Android 10+ 由 CameraX 直接写入 MediaStore,避免完整视频二次复制,此时结果 pathcontent:// URI;不保存相册时返回缓存文件路径。

隐私、权限声明

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

iOS: NSCameraUsageDescription, NSMicrophoneUsageDescription, NSPhotoLibraryAddUsageDescription; Android: 相机、麦克风、存储权限

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

插件不采集任何数据

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