OpenHarmony 原生 Camera 相机拍摄实现(API Version23 + 适配版)
摘要
Camera 模块为系统原生相机能力,支持实时预览、拍照保存图片、录像等功能,适用于扫码、头像拍摄、单据上传等业务场景。API Version23 重构相机生命周期、设备释放、帧数据回调、存储写入逻辑,修复低版本相机占用无法释放、多次打开黑屏、拍摄图片保存失败、切换前后摄像头崩溃、后台驻留相机耗电等兼容缺陷。旧项目升级 API23 后常出现:页面退出相机未释放导致下次打开黑屏、拍摄无文件、预览画面拉伸变形、无相机权限直接闪退等问题。本文基于 DevEco Studio 适配 OpenHarmony API23 及以上版本,封装相机通用工具类,实现实时预览、拍照保存至沙盒、前后摄像头切换三大核心功能,搭配拍摄页面完整实战代码,输出权限校验、设备资源释放、图片持久化规范,汇总版本升级兼容故障解决方案,为鸿蒙相机拍摄业务开发提供标准化实操模板。
关键词
OpenHarmony;ArkUI;API Version23;camera;相机预览;拍照;媒体存储;资源释放
一、引言
1.1 相机开发背景
头像拍摄、证件上传、扫码识别、视频录制等场景都需要调用硬件相机。系统 camera 模块直接操作硬件摄像头,相比相册选择能实时取景拍摄,交互更贴合拍照类业务。相机属于独占硬件资源,页面销毁必须主动释放设备,否则会持续占用硬件导致其他应用无法调用相机。 OpenHarmony API Version23 针对 camera 底层硬件调度引擎完成全面升级,核心变更点:
- 标准化 CameraDevice 完整生命周期:创建→打开预览→拍摄→关闭释放,缺失步骤直接报错;
- 新增硬件占用强回收机制,页面销毁未释放相机则系统强制断开硬件;
- 统一拍摄图片沙盒写入逻辑,修复图片丢失、路径无效、无写入权限问题;
- 重构 Surface 预览绑定规则,修复预览画面拉伸、黑屏、画面倒置;
- 强化 CAMERA 动态权限强校验,未授权调用直接抛出可捕获异常;
- 区分前后摄像头设备 ID,切换摄像头自动销毁重建预览流,无画面卡顿。
大量 API9~11 旧项目升级后,二次进入相机页面黑屏、拍摄图片找不到、切换摄像头闪退,根源是未完整释放 CameraDevice 硬件资源,因此掌握 API23 标准相机生命周期与资源释放规范是拍摄类功能必备技能。
1.2 开发环境与测试场景
开发工具:DevEco Studio 5.0 及以上 适配系统:OpenHarmony API Version23、HarmonyOS NEXT 导入模块:@ohos.multimedia.camera、@ohos.file.fs、@ohos.file.common 前置依赖:动态 CAMERA 权限、存储写入权限 测试场景:实时相机预览、拍摄照片保存本地、前后摄像头切换、页面退出释放相机、无权限拦截提示
二、API23+ camera 相机核心 API 与版本变更说明
2.1 核心对象生命周期顺序
- CameraManager:相机管理器,获取设备列表
- CameraDevice:摄像头硬件实例(核心独占资源,用完必须 close)
- PreviewOutput:预览输出流,绑定 XComponent 渲染画面
- CaptureOutput:拍照输出流,处理拍摄图片保存
2.2 关键核心方法
ets
import camera from '@ohos.multimedia.camera'
// 获取相机管理器
const camMgr = camera.getCameraManager(getContext(this))
// 获取全部摄像头设备ID列表
const deviceIds = camMgr.getSupportedCameraDeviceIds()
// 打开摄像头硬件
const cameraDev = await camMgr.openCamera(deviceId)
// 创建预览输出
const preview = await camMgr.createPreviewOutput(surfaceId, cameraDev)
// 创建拍照输出
const capture = await camMgr.createCaptureOutput(fileFd, cameraDev)
// 开始预览
preview.start()
// 执行拍照
await capture.capture()
// 停止预览、关闭硬件(必须执行)
preview.stop()
cameraDev.close()
2.3 XComponent 预览容器
相机画面依赖 XComponent 组件承载,通过 onSurfaceCreated 获取 surfaceId 绑定预览流。
2.4 API23 废弃与强制约束
- 废弃页面销毁不执行 cameraDev.close () 写法,硬件长期占用造成黑屏;
- 禁止多页面同时持有 CameraDevice 实例,同一时间仅允许一个页面使用相机;
- 拍摄图片必须传入文件文件描述符 fd,不支持直接传入绝对路径;
- 必须前置动态申请 ohos.permission.CAMERA 权限,无权限直接捕获异常;
- 切换摄像头必须先关闭当前 CameraDevice,再重新创建新设备实例;
- 后台切页必须停止预览并关闭相机,避免持续耗电、硬件占用。
三、API23 标准基础示例代码
3.1 基础相机打开与预览流程
ets
import camera from '@ohos.multimedia.camera'
import common from '@ohos.app.ability.common'
let cameraDev: camera.CameraDevice | null = null
let previewOut: camera.PreviewOutput | null = null
async function openCamera(ctx: common.UIAbilityContext, surfaceId: string, deviceId: string) {
const camMgr = camera.getCameraManager(ctx)
// 打开硬件摄像头
cameraDev = await camMgr.openCamera(deviceId)
// 创建预览输出绑定XComponent画布
previewOut = await camMgr.createPreviewOutput(surfaceId, cameraDev)
// 启动预览画面
await previewOut.start()
}
// 释放全部相机资源(页面退出必调用)
async function releaseCamera() {
if (previewOut) {
await previewOut.stop()
previewOut = null
}
if (cameraDev) {
await cameraDev.close()
cameraDev = null
}
}
3.2 基础拍照并保存沙盒文件
ets
import fs from '@ohos.file.fs'
import camera from '@ohos.multimedia.camera'
async function takePhoto(ctx: common.UIAbilityContext) {
const camMgr = camera.getCameraManager(ctx)
// 创建沙盒图片文件
const filePath = ctx.filesDir + "/photo_" + Date.now() + ".jpg"
const fd = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY)
// 创建拍照输出流
const captureOut = await camMgr.createCaptureOutput(fd, cameraDev!)
// 执行拍摄
await captureOut.capture()
// 关闭文件描述符
fs.closeSync(fd)
return filePath
}
四、完整实战案例(相机工具类 + 拍摄页面)
4.1 实战一:相机工具封装 utils/camera_util.ets
ets
import camera from '@ohos.multimedia.camera'
import fs from '@ohos.file.fs'
import common from '@ohos.app.ability.common'
import promptAction from '@ohos.promptAction'
class CameraUtil {
private static instance: CameraUtil
private ctx: common.UIAbilityContext | null = null
private cameraDev: camera.CameraDevice | null = null
private previewOut: camera.PreviewOutput | null = null
private curDeviceId: string = ""
static getInstance(): CameraUtil {
if (!CameraUtil.instance) {
CameraUtil.instance = new CameraUtil()
}
return CameraUtil.instance
}
setContext(context: common.UIAbilityContext) {
this.ctx = context
}
// 获取前后摄像头ID,0后置,1前置
getCameraIds(): string[] {
if (!this.ctx) return []
const mgr = camera.getCameraManager(this.ctx)
return mgr.getSupportedCameraDeviceIds()
}
// 初始化相机预览
async initCamera(surfaceId: string, deviceId: string): Promise<boolean> {
if (!this.ctx) return false
// 先释放已有相机
await this.release()
try {
const mgr = camera.getCameraManager(this.ctx)
this.curDeviceId = deviceId
this.cameraDev = await mgr.openCamera(deviceId)
this.previewOut = await mgr.createPreviewOutput(surfaceId, this.cameraDev)
await this.previewOut.start()
return true
} catch (err) {
promptAction.showToast({ message: "相机打开失败,请检查权限" })
console.error("initCamera error", err)
return false
}
}
// 执行拍照,返回图片沙盒路径
async takePicture(): Promise<string> {
if (!this.ctx || !this.cameraDev) return ""
try {
const mgr = camera.getCameraManager(this.ctx)
const fileName = "cam_" + Date.now() + ".jpg"
const fullPath = this.ctx.filesDir + "/" + fileName
const fd = fs.openSync(fullPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY)
const captureOut = await mgr.createCaptureOutput(fd, this.cameraDev)
await captureOut.capture()
fs.closeSync(fd)
promptAction.showToast({ message: "拍摄完成" })
return fullPath
} catch (err) {
promptAction.showToast({ message: "拍摄失败" })
console.error("takePicture error", err)
return ""
}
}
// 释放相机全部硬件资源(页面销毁强制调用)
async release() {
if (this.previewOut) {
try {
await this.previewOut.stop()
} catch (e) {}
this.previewOut = null
}
if (this.cameraDev) {
try {
await this.cameraDev.close()
} catch (e) {}
this.cameraDev = null
}
}
}
export default CameraUtil.getInstance()
4.2 实战二:相机拍摄页面(XComponent 预览 + 切换摄像头 + 拍照)
ets
import CameraUtil from '../utils/camera_util'
import PermissionUtil, { PERMISSION } from '../utils/permission_util'
import ImagePickerUtil from '../utils/image_picker'
@Entry
@Component
struct CameraPage {
@State surfaceId: string = ""
@State photoPath: ResourceStr = ""
deviceList: string[] = []
@State currentIndex: number = 0
async aboutToAppear() {
// 绑定上下文并校验相机权限
PermissionUtil.setContext(getContext(this))
const hasCamPerm = await PermissionUtil.requestSingle(PERMISSION.CAMERA, "相机")
if (!hasCamPerm) return
CameraUtil.setContext(getContext(this))
this.deviceList = CameraUtil.getCameraIds()
}
// XComponent画布创建完成,初始化相机预览
onSurfaceReady(surface: string) {
this.surfaceId = surface
if (this.deviceList.length > 0) {
CameraUtil.initCamera(this.surfaceId, this.deviceList[this.currentIndex])
}
}
// 切换前后摄像头
async switchCamera() {
this.currentIndex = this.currentIndex === 0 ? 1 : 0
await CameraUtil.initCamera(this.surfaceId, this.deviceList[this.currentIndex])
}
// 执行拍照
async shotPhoto() {
const path = await CameraUtil.takePicture()
if (path) this.photoPath = path
}
build() {
Column() {
// 相机预览画布
XComponent()
.width("100%")
.layoutWeight(1)
.onSurfaceCreated((surface) => this.onSurfaceReady(surface))
// 底部操作按钮栏
Row({ space: 30 }) {
Button("切换摄像头").width(140).height(48).onClick(() => this.switchCamera())
Button("拍照").width(140).height(48).backgroundColor("#f56c6c").onClick(() => this.shotPhoto())
}
.width("100%")
.height(80)
.justifyContent(FlexAlign.Center)
// 拍摄预览缩略图
if (this.photoPath) {
Image(this.photoPath)
.width(80)
.height(80)
.position({ x: 10, y: -90 })
.borderRadius(8)
}
}
.width("100%")
.height("100%")
}
async aboutToDisappear() {
// 页面退出强制释放相机硬件资源,API23硬性要求
await CameraUtil.release()
}
}
五、API23+ 相机适配与硬件性能优化规范
5.1 硬件资源释放强制规范
- 页面
aboutToDisappear必须调用工具 release () 停止预览、关闭 CameraDevice; - 切换摄像头先关闭当前设备,再创建新预览流,避免硬件冲突;
- 应用切后台 onBackground 主动释放相机,前台返回再重新初始化。
5.2 权限与存储规范
- 打开相机前动态申请 CAMERA 权限,无权限阻断相机初始化流程;
- 拍摄图片保存至应用沙盒 filesDir 目录,无需额外存储权限;
- 拍照文件必须使用 fs 文件描述符 fd 传入 CaptureOutput,禁止直接传路径。
5.3 预览画面适配规范
- XComponent 铺满页面宽高,避免画面拉伸、黑边;
- 设备 ID 0 默认后置主摄,1 前置自拍,自动区分设备列表;
- 初始化失败捕获异常弹窗提示,不直接闪退应用。
5.4 性能耗电规范
- 长时间停留相机页面不频繁重复创建 / 销毁预览;
- 页面退出、切后台立刻关闭相机硬件,停止图像解码节省电量;
- 不循环连续调用拍照,增加按钮防抖锁防止重复拍摄。
六、API23 升级高频兼容问题与解决方案
问题 1:第一次打开相机正常,返回再次进入页面黑屏无画面 解决:页面销毁执行 release 完整关闭 CameraDevice 硬件,下次进入重新初始化。
问题 2:点击拍照无图片,沙盒目录找不到照片文件 解决:使用 fs.openSync 获取文件描述符 fd 传入拍摄输出,不能直接传文件路径字符串。
问题 3:切换前后摄像头画面卡顿、直接闪退 解决:切换前先释放当前预览与摄像头设备,再新建预览流。
问题 4:无相机权限进入页面直接崩溃 解决:页面加载前置动态权限校验,未授予权限直接阻断相机初始化逻辑。
问题 5:切后台后再切回,相机画面冻结不动 解决:onBackground 释放相机,onForeground 重新调用 initCamera 重建预览。
问题 6:多次打开相机后系统提示相机被其他应用占用 解决:保证所有退出分支都执行 release,无遗漏硬件关闭逻辑。
七、总结
camera 原生相机模块是鸿蒙硬件拍摄标准实现方案,API Version23 严格标准化摄像头硬件生命周期,强制要求页面销毁释放 CameraDevice 硬件资源,解决旧版硬件长期占用、黑屏、拍摄图片丢失、切换摄像头崩溃等核心问题。项目封装独立相机工具类,统一管理预览、拍照、设备释放逻辑,搭配 XComponent 预览画布、动态权限校验,完整实现前后摄像头切换、拍照保存本地核心业务。
更多推荐
所有评论(0)