更新记录

1.0.1(2026-09-28)

  • 优化鸿蒙端

1.0.0(2026-09-28)

  • 新版发布

平台兼容性

uni-app x(5.01)

Chrome Safari Android iOS 鸿蒙 微信小程序
× × √ √ √ ×

yt-unix-camera

特别提醒

  • 购买本插件前,请先试用,请先试用,请先试用,确认满足需求之后再行购买。虚拟物品一旦购买之后无法退款。
  • 如有使用上的疑问、bug,可以进交流群联系作者;
  • 请在合法范围内使用,若使用本插件做非法开发,本方概不负责;
  • 可扫描右侧二维码安装Android示例demo,体验插件功能
  • iOS使用示例demo测试时,要先修改manifest中项目名称后再打包(名称长度尽量短一点),不然会因为名称过长,导致App启动不了。
  • 插件需先引入再打自定义基座后使用
  • 修改插件原生代码(如 UTS / Kotlin / Swift)后,需重新制作自定义调试基座,热更新通常不生效
  • 本插件是标准式组件必须在uniappx项目使用,uniapp项目看这里

主要能力:

  • 自定义尺寸的原生相机预览
  • 普通拍照、拍照后裁剪/旋转编辑
  • 视频录制,可选择是否录制麦克风声音、是否保存到相册
  • 指定 2160p、1080p、720p、480p 分辨率和目标帧率
  • 请求的分辨率/帧率组合不可用时自动降级
  • 切换前后摄像头、开关手电筒
  • 点击预览对焦、双指缩放、接口控制缩放倍率
  • 删除插件生成的沙盒照片或视频文件
  • Android 拍照方向兼容处理(读取并校正 EXIF 方向)

运行要求

  • HBuilderX 4.31 或更高版本
  • uni-app x App Android / App iOS / App HarmonyOS
  • 本插件包含原生配置与依赖,调试时应制作自定义基座
  • 标准式 UTS 组件不支持旧版 uni-app/nvue 项目

模块目录名、插件 ID 和组件名均为 yt-unix-camera。

最小示例

组件通过 easycom 自动注册,无需手动写入 components:

<template>
  <yt-unix-camera
    ref="cameraRef"
    style="width: 750rpx; height: 1000rpx;"
    camera-position="back"
    @capturePhotoHandle="onCapturePhoto"
    @recordingHandle="onRecordingFinished"
    @recordingDurationUpdateHandler="onRecordingDuration"
  ></yt-unix-camera>
</template>

<script setup lang="uts">
const cameraRef = ref<YtUnixCameraComponentPublicInstance | null>(null)

function takePhoto() {
  // 参数一:是否保存到系统相册
  // 参数二:是否在拍照后进入编辑页
  cameraRef.value?.capturePhotoAction(true, false)
}

function takeEditablePhoto() {
  cameraRef.value?.capturePhotoAction(true, true)
}

function onCapturePhoto(event: UniNativeViewEvent) {
  // event.detail: { status, msg, path }
  console.log(event.detail)
}

function onRecordingFinished(event: UniNativeViewEvent) {
  // 指定画质录像完成时还会返回 resolution、frameRate
  console.log(event.detail)
}

function onRecordingDuration(event: UniNativeViewEvent) {
  // event.detail.durationString,例如 00:00:08
  console.log(event.detail)
}
</script>

组件必须设置明确的宽高,否则 native-view 没有显示相机预览的布局区域。

属性

属性 类型 默认值 说明
cameraPosition string back 初始摄像头;支持 back、front。仅在原生相机创建时读取,运行中请调用切换方法。

拍照

普通拍照和可编辑拍照已合并为一个方法:

// 普通拍照
cameraRef.value?.capturePhotoAction(true, false)

// 拍照后显示裁剪/旋转编辑页;完成后仍通过 capturePhotoHandle 返回
cameraRef.value?.capturePhotoAction(true, true)

// 第二个参数默认 false,因此普通拍照也可以只传一个参数
cameraRef.value?.capturePhotoAction(true)

方法签名:

capturePhotoAction(saveToAlbum: boolean, enableEdit: boolean = false): void
  • saveToAlbum:是否把最终照片同时保存到系统相册。
  • enableEdit:false 为普通拍照;true 为编辑完成后返回照片。
  • 编辑页取消后不会触发成功回调,可以继续拍照。
  • 照片沙盒路径通过 capturePhotoHandle 的 event.detail.path 返回。

录像

默认录像

第一次调用开始录像,再次调用停止录像:

const saveToAlbum = true
const recordAudio = true
cameraRef.value?.toggleRecordingAction(saveToAlbum, recordAudio)

Android 默认请求 720p、由系统选择帧率;HarmonyOS 默认请求 720p 并优先使用 30fps,设备不支持时自动降级;iOS 沿用 high 会话预设并由系统选择帧率。

指定输出格式

cameraRef.value?.toggleRecordingWithOptions(true, true, 'mp4')
  • Android、HarmonyOS 仅支持 mp4。
  • iOS 支持 mov、mp4。

指定分辨率和帧率

// 请求 1080p / 60fps;设备不支持时自动降级。
cameraRef.value?.toggleRecordingWithResolution(
  true,       // 是否保存到相册
  true,       // 是否录制麦克风
  '1080p',    // 2160p、1080p、720p、480p
  'mp4',      // Android/HarmonyOS: mp4;iOS: mov 或 mp4
  60          // 0=系统选择;常用 24、30、60
)

resolution 和 frameRate 是目标值,并不保证成片一定使用该组合。插件会优先保留请求帧率并逐级降低分辨率;仍不可用时继续选择设备可用的较低组合。

录像完成后应以回调中的实际参数为准:

function onRecordingFinished(event: UniNativeViewEvent) {
  const detail = event.detail
  // detail.resolution:实际分辨率档位
  // detail.frameRate:实际/目标帧率;0 表示由系统选择
  console.log(detail)
}

其他组件方法

页面通过 YtUnixCameraComponentPublicInstance 调用下列公开方法:

方法 参数 说明
switchCameraAction 无 切换前后摄像头
capturePhotoAction saveToAlbum, enableEdit=false 普通/可编辑拍照统一入口
toggleTorchAction 无 开关手电筒
toggleRecordingAction saveToAlbum, recordAudio 默认参数录像/停止
toggleRecordingWithOptions saveToAlbum, recordAudio, outputFormat 指定封装格式录像/停止
toggleRecordingWithResolution saveToAlbum, recordAudio, resolution, outputFormat, frameRate 指定画质录像/停止
startRunning 无 恢复采集;iOS/HarmonyOS 显式启动,Android 跟随 Activity 生命周期
stopRunning 无 暂停采集;iOS/HarmonyOS 显式停止,Android 跟随 Activity 生命周期
setZoomFactorAction factor, animated 设置缩放倍率,原生层自动限制到设备范围
getZoomFactorAction 无 在控制台输出当前倍率
getMinZoomFactorAction 无 在控制台输出设备最小倍率
getMaxZoomFactorAction 无 在控制台输出设备最大倍率
resetZoomFactorAction animated 恢复 1 倍缩放

事件

标准式组件在 Android/iOS/HarmonyOS 使用完全相同的事件字段,不需要条件编译三套处理函数。

注意:不同平台的 event.detail 运行时对象类型可能不同。跨端代码不要调用 event.detail.getString() 或把 detail 强制转换成自定义 class;可直接读取公开字段,或先用 JSON.stringify/JSON.parse 转成纯 UTS type。项目示例采用后一种方式。

capturePhotoHandle

event.detail:

{
  status: '0',       // 0 成功,1 失败
  msg: '拍照成功',
  path: '/.../photo.jpg'
}

recordingHandle

event.detail:

{
  status: '0',
  msg: '录制成功',
  path: '/.../video.mp4',
  resolution: '1080p', // 指定画质录像时返回实际档位
  frameRate: 60         // 0 表示系统选择
}

recordingDurationUpdateHandler

event.detail:

{
  durationString: '00:00:08'
}

缩放、对焦和手电筒

cameraRef.value?.switchCameraAction()
cameraRef.value?.toggleTorchAction()
cameraRef.value?.setZoomFactorAction(2.0, true)
cameraRef.value?.resetZoomFactorAction(true)
  • 预览区域默认支持单击对焦和双指捏合缩放。
  • 前置摄像头或部分设备没有可用闪光灯,调用手电筒方法可能不会改变硬件状态。
  • 切换摄像头后,可用分辨率、帧率和缩放范围都可能改变。

删除沙盒文件

import { deleteFile } from '@/uni_modules/yt-unix-camera'

deleteFile({
  path: '/absolute/path/photo.jpg',
  success: result => {
    console.log(result.message)
  },
  fail: error => {
    console.log(error.errMsg)
  },
  complete: result => {
    console.log(result)
  }
})

删除的是回调返回的本地沙盒文件;如果拍摄时同时保存到了系统相册,删除沙盒文件不会删除相册副本。

页面生命周期建议

onReady(() => {
  cameraRef.value?.startRunning()
})

onShow(() => {
  cameraRef.value?.startRunning()
})

onHide(() => {
  cameraRef.value?.stopRunning()
})

组件卸载时会自动释放原生相机、录像、编辑页和相关回调,不需要页面手动销毁。

权限

插件原生配置声明了下列权限:

  • Android:android.permission.CAMERA、android.permission.RECORD_AUDIO
  • iOS:NSCameraUsageDescription、NSMicrophoneUsageDescription、NSPhotoLibraryAddUsageDescription
  • HarmonyOS:ohos.permission.CAMERA、ohos.permission.MICROPHONE

相机权限在组件初始化时申请;录制声音时才会使用麦克风权限。HarmonyOS 保存系统相册使用系统安全资产创建弹窗,不声明 WRITE_IMAGEVIDEO。系统相册权限行为取决于系统版本。

完整示例代码

<template>
    <view class="page">
        <!--
            标准式组件通过 easycom 自动引入,无需在页面 components 中注册。
            组件必须设置明确的宽高,否则 native-view 没有可用于显示预览的区域。
        -->
        <!--
            标准组件启用样式隔离后,页面 class 不一定会作用到组件内部根节点。
            因此使用普通 view 固定预览区域,再通过内联样式明确设置组件自身宽高,
            避免 native-view 被 flex 拉伸并把下面的操作区挤出屏幕。
        -->
        <view class="camera-container">
            <yt-unix-camera
                ref="cameraRef"
                style="width: 750rpx; height: 760rpx;"
                camera-position="back"
                @capturePhotoHandle="onCapturePhoto"
                @recordingHandle="onRecordingFinished"
                @recordingDurationUpdateHandler="onRecordingDuration"
            ></yt-unix-camera>
        </view>

        <!-- 录像计时来自原生层,每秒更新一次。 -->
        <view v-if="isRecording" class="recording-badge">
            <view class="recording-dot"></view>
            <text class="recording-time">{{ recordingTime }}</text>
        </view>

        <scroll-view class="control-panel" scroll-y="true">
            <text class="section-title">拍照</text>
            <view class="button-row">
                <button class="action-button" size="mini" @click="capturePhoto(false)">普通拍照</button>
                <button class="action-button" size="mini" @click="capturePhoto(true)">拍照后编辑</button>
            </view>

            <text class="section-title">录像</text>
            <view class="button-row">
                <button class="action-button" size="mini" @click="toggleDefaultRecording">
                    {{ isRecording ? '停止录像' : '默认录像' }}
                </button>
                <button class="action-button primary-button" size="mini" @click="toggle1080p60Recording">
                    {{ isRecording ? '停止录像' : '1080p / 60fps' }}
                </button>
            </view>
            <text class="hint">
                1080p/60fps 是目标值;设备或当前摄像头不支持时,插件会自动降级,实际参数在录像完成回调中返回。
            </text>

            <text class="section-title">相机控制</text>
            <view class="button-row">
                <button class="action-button" size="mini" @click="switchCamera">切换摄像头</button>
                <button class="action-button" size="mini" @click="toggleTorch">
                    {{ torchEnabled ? '关闭手电筒' : '打开手电筒' }}
                </button>
            </view>
            <view class="button-row">
                <button class="action-button" size="mini" @click="zoomOut">缩小</button>
                <button class="action-button" size="mini" @click="resetZoom">重置 1x</button>
                <button class="action-button" size="mini" @click="zoomIn">放大</button>
            </view>
            <text class="hint">当前缩放:{{ currentZoom }}x;也可以直接在预览区域双指缩放、点击对焦。</text>

            <view v-if="lastFilePath.length > 0" class="result-card">
                <text class="section-title">最近一次结果</text>
                <text class="result-text">{{ lastMessage }}</text>
                <text class="path-text">{{ lastFilePath }}</text>
                <image
                    v-if="lastResultIsPhoto"
                    class="photo-preview"
                    :src="lastFileUrl"
                    mode="aspectFit"
                ></image>
                <button class="delete-button" size="mini" @click="deleteLastFile">删除本地文件</button>
            </view>
        </scroll-view>
    </view>
</template>

<script lang="uts">
    import { deleteFile } from '@/uni_modules/yt-unix-camera'

    /**
     * 三端 native-view 事件的可序列化数据结构。
     *
     * Android 的 event.detail 在运行时可能是 UTSJSONObject,iOS 则是普通对象;不能直接调用
     * getString/getNumber,也不能强制转换成自定义 class。统一 JSON 序列化后再解析成 type,
     * 可以同时兼容 Android、iOS 和 HarmonyOS。
     */
    type CameraNativeEventDetail = {
        status?: string
        msg?: string
        path?: string
        resolution?: string
        frameRate?: number
        durationString?: string
        albumUri?: string
    }

    export default {
        data() {
            return {
                isRecording: false,
                recordingTime: '00:00:00',
                torchEnabled: false,
                currentZoom: 1.0,
                lastFilePath: '',
                lastFileUrl: '',
                lastMessage: '尚无结果',
                lastResultIsPhoto: false
            }
        },

        /** 页面首次渲染完成后确保 iOS 采集会话处于运行状态。 */
        onReady() {
            (this.$refs['cameraRef'] as YtUnixCameraComponentPublicInstance | null)?.startRunning()
        },

        /** 页面重新显示时恢复预览。Android 由 Activity 生命周期自动恢复。 */
        onShow() {
            (this.$refs['cameraRef'] as YtUnixCameraComponentPublicInstance | null)?.startRunning()
        },

        /** 页面隐藏时停止 iOS 采集,避免后台继续占用摄像头。 */
        onHide() {
            (this.$refs['cameraRef'] as YtUnixCameraComponentPublicInstance | null)?.stopRunning()
        },

        methods: {
            /**
             * 普通拍照和可编辑拍照已经合并为同一个方法。
             * enableEdit=false:普通拍照;enableEdit=true:拍照后进入编辑页。
             */
            capturePhoto(enableEdit: boolean) {
                const saveToAlbum = true
                ;(this.$refs['cameraRef'] as YtUnixCameraComponentPublicInstance | null)?.capturePhotoAction(
                    saveToAlbum,
                    enableEdit
                )
            },

            /** 兼容旧录像调用:不指定分辨率和帧率。再次调用同一方法会停止录像。 */
            toggleDefaultRecording() {
                const saveToAlbum = true
                const recordAudio = true
                this.isRecording = !this.isRecording
                ;(this.$refs['cameraRef'] as YtUnixCameraComponentPublicInstance | null)?.toggleRecordingAction(
                    saveToAlbum,
                    recordAudio
                )
            },

            /**
             * 请求 1080p/60fps 录像。
             * Android 仅支持 mp4;iOS 同时支持 mov/mp4,所以跨端示例统一使用 mp4。
             * 设备不支持 1080p+60fps 组合时会自动降级,不能只用请求值判断成片参数。
             */
            toggle1080p60Recording() {
                const saveToAlbum = true
                const recordAudio = true
                const resolution = '1080p'
                const outputFormat = 'mp4'
                const frameRate = 60
                this.isRecording = !this.isRecording
                ;(this.$refs['cameraRef'] as YtUnixCameraComponentPublicInstance | null)?.toggleRecordingWithResolution(
                    saveToAlbum,
                    recordAudio,
                    resolution,
                    outputFormat,
                    frameRate
                )
            },

            switchCamera() {
                ;(this.$refs['cameraRef'] as YtUnixCameraComponentPublicInstance | null)?.switchCameraAction()
                // 切换摄像头后不假设新摄像头也有手电筒,重置页面显示状态。
                this.torchEnabled = false
            },

            toggleTorch() {
                ;(this.$refs['cameraRef'] as YtUnixCameraComponentPublicInstance | null)?.toggleTorchAction()
                this.torchEnabled = !this.torchEnabled
            },

            /** 示例把按钮缩放范围限制为 1x~5x;原生层仍会按设备真实范围再次限制。 */
            zoomIn() {
                this.currentZoom = Math.min(this.currentZoom + 0.5, 5.0)
                ;(this.$refs['cameraRef'] as YtUnixCameraComponentPublicInstance | null)?.setZoomFactorAction(
                    this.currentZoom,
                    true
                )
            },

            zoomOut() {
                this.currentZoom = Math.max(this.currentZoom - 0.5, 1.0)
                ;(this.$refs['cameraRef'] as YtUnixCameraComponentPublicInstance | null)?.setZoomFactorAction(
                    this.currentZoom,
                    true
                )
            },

            resetZoom() {
                this.currentZoom = 1.0
                ;(this.$refs['cameraRef'] as YtUnixCameraComponentPublicInstance | null)?.resetZoomFactorAction(true)
            },

            /**
             * 将 native-view 的平台运行时对象转换成纯 UTS type。
             * JSON 转换只包含字符串/数字字段,不会携带任何平台原生实例。
             */
            parseEventDetail(event: UniNativeViewEvent): CameraNativeEventDetail {
                try {
                    const detail = JSON.parse<CameraNativeEventDetail>(JSON.stringify(event.detail))
                    if (detail != null) {
                        return detail
                    }
                    const emptyDetail: CameraNativeEventDetail = {}
                    return emptyDetail
                } catch (_error) {
                    return {}
                }
            },

            /** 保存最近结果,并用 toast 告知成功或失败。 */
            showResult(detail: CameraNativeEventDetail, isPhoto: boolean) {
                const status = detail.status ?? '1'
                const message = detail.msg ?? '操作失败'
                const path = detail.path ?? ''
                this.lastMessage = message
                this.lastResultIsPhoto = isPhoto && status == '0'
                if (path.length > 0) {
                    this.lastFilePath = path
                    this.lastFileUrl = path.startsWith('file://') ? path : `file://${path}`
                }
                uni.showToast({
                    title: message,
                    icon: status == '0' ? 'success' : 'none'
                })
            },

            /** 三端事件先经过 JSON 标准化,避免依赖某个平台的 detail 运行时类型。 */
            onCapturePhoto(event: UniNativeViewEvent) {
                this.showResult(this.parseEventDetail(event), true)
            },

            onRecordingFinished(event: UniNativeViewEvent) {
                const detail = this.parseEventDetail(event)
                this.isRecording = false
                this.recordingTime = '00:00:00'
                this.showResult(detail, false)

                const status = detail.status ?? '1'
                const resolution = detail.resolution ?? ''
                if (status == '0' && resolution.length > 0) {
                    const actualFrameRate = detail.frameRate ?? 0
                    const frameRateText = actualFrameRate == 0 ? '系统默认帧率' : `${actualFrameRate}fps`
                    console.log(`实际录制参数:${resolution}/${frameRateText}`)
                }
            },

            onRecordingDuration(event: UniNativeViewEvent) {
                this.recordingTime = this.parseEventDetail(event).durationString ?? '00:00:00'
            },

            /** deleteFile 是模块 API,可删除拍照或录像回调返回的沙盒文件。 */
            deleteLastFile() {
                if (this.lastFilePath.length == 0) return
                deleteFile({
                    path: this.lastFilePath,
                    success: result => {
                        uni.showToast({ title: result.message, icon: 'success' })
                        this.lastFilePath = ''
                        this.lastFileUrl = ''
                        this.lastResultIsPhoto = false
                    },
                    fail: error => {
                        uni.showToast({ title: error.errMsg, icon: 'none' })
                    }
                })
            }
        }
    }
</script>

<style>
    .page {
        flex: 1;
        flex-direction: column;
        background-color: #111827;
    }

    .camera-container {
        width: 750rpx;
        height: 760rpx;
        flex-shrink: 0;
        background-color: #000000;
    }

    .recording-badge {
        position: absolute;
        top: 24rpx;
        left: 275rpx;
        width: 200rpx;
        height: 64rpx;
        flex-direction: row;
        align-items: center;
        justify-content: center;
        border-radius: 32rpx;
        background-color: rgba(0, 0, 0, 0.65);
    }

    .recording-dot {
        width: 18rpx;
        height: 18rpx;
        margin-right: 14rpx;
        border-radius: 9rpx;
        background-color: #ef4444;
    }

    .recording-time {
        font-size: 28rpx;
        color: #ffffff;
    }

    .control-panel {
        flex: 1;
        width: 750rpx;
        padding: 24rpx;
        background-color: #f3f4f6;
    }

    .section-title {
        margin-top: 18rpx;
        margin-bottom: 14rpx;
        font-size: 30rpx;
        font-weight: 600;
        color: #111827;
    }

    .button-row {
        width: 702rpx;
        margin-bottom: 14rpx;
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
    }

    .action-button {
        min-width: 206rpx;
        margin: 0;
        color: #111827;
        background-color: #ffffff;
    }

    .primary-button {
        color: #ffffff;
        background-color: #2563eb;
    }

    .hint {
        margin-bottom: 18rpx;
        font-size: 24rpx;
        line-height: 36rpx;
        color: #6b7280;
    }

    .result-card {
        margin-top: 18rpx;
        margin-bottom: 80rpx;
        padding: 20rpx;
        border-radius: 16rpx;
        background-color: #ffffff;
    }

    .result-text {
        font-size: 26rpx;
        color: #111827;
    }

    .path-text {
        margin-top: 10rpx;
        font-size: 22rpx;
        line-height: 30rpx;
        color: #6b7280;
    }

    .photo-preview {
        width: 662rpx;
        height: 360rpx;
        margin-top: 18rpx;
        background-color: #111827;
    }

    .delete-button {
        margin-top: 18rpx;
        margin-left: 0;
        color: #ffffff;
        background-color: #dc2626;
    }
</style>

更多好用实惠插件

隐私、权限声明

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

Android:android.permission.CAMERA、android.permission.RECORD_AUDIO;iOS:NSCameraUsageDescription、NSMicrophoneUsageDescription、NSPhotoLibraryAddUsageDescription;HarmonyOS:ohos.permission.CAMERA、ohos.permission.MICROPHONE

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

插件不采集任何数据

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

无

暂无用户评论。