更新记录

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

注意事项

  1. 仅支持 nvue 页面:本组件为原生组件,只能在 nvue 页面中使用。
  2. 页面卸载:在页面卸载前(beforeUnmount/onBeforeUnmount)建议手动调用 stopRecording() 停止录制,组件会在 NVBeforeUnload 中自动释放资源。
  3. 权限检查:组件在 Android 端会自动请求相机和麦克风权限;iOS 端需要在 Info.plist 中手动配置权限描述。
  4. maxDuration 参数:传入 0 表示不限制录制时长;设置最大时长后,到达时间会自动停止录制并触发 onRecordingFinished 事件。
  5. 录制状态同步isRecording 状态应由 onRecordingFinished 事件驱动更新,而非仅依赖前端标记,以确保与原生状态一致。
  6. saveToAlbum:Android 端录制始终先写入缓存目录,录制完成后如需保存到相册再拷贝到 MediaStore,避免权限问题导致静默失败。

隐私、权限声明

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

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

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

插件不采集任何数据

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