《HarmonyOS技术精讲-Camera Kit(相机服务)第3篇:实现相机预览功能》

开篇
Camera Kit的预览功能是几乎所有相机应用的基础模块。但在HarmonyOS NEXT开发中,很多人第一次接触预览时都会遇到一个现象:官方示例能运行,但放到实际项目里,XComponent显示黑屏、Session创建失败、权限回调处理不当。这些问题不是因为API复杂,而是因为几个关键细节没有处理到位。
这篇文章从零搭建一个完整的预览模块,代码全部可运行,重点处理生命周期、状态同步、权限管理这些实际开发里避不开的问题。
核心组件
Camera Kit的预览流程涉及四个关键对象:
| 组件 | 职责 | 生命周期 |
|---|---|---|
| CameraManager | 相机管理入口 | 应用级单例 |
| CameraInput | 相机设备输入 | 随会话创建和销毁 |
| PreviewOutput | 预览输出流 | 随预览会话 |
| CaptureSession | 会话控制器 | 管理输入输出绑定 |
这个架构的设计思路是:输入和输出分离,通过会话管理连接成一整条数据通路。PreviewOutput本身不负责渲染,它只输出画面数据,需要XComponent作为接收端。
环境说明
DevEco Studio版本:DevEco Studio 6.1.0及以上
HarmonyOS SDK版本:HarmonyOS 6.1.0(23)及以上
目标设备:手机(真机)
完整实现
1. 权限配置
在module.json5中添加相机权限。这里要注意:必须同时声明ohos.permission.CAMERA和ohos permission.MICROPHONE,否则在某些设备上预览流会无声。
json
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:camera_reason",
"usedScene": {
"abilities": ["EntryAbility"]
}
},
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:microphone_reason",
"usedScene": {
"abilities": ["EntryAbility"]
}
}
]
}
}
2. 预览页面布局
XComponent是ArkUI里的原生视图容器,可以用来承载Camera Kit的预览数据。这里指定type为suraface,libraryname可以填空字符串。
typescript
@Entry
@Component
struct CameraPreviewPage {
@State isCameraReady: boolean = false
private xComponentController: XComponentController = new XComponentController()
build() {
Column() {
XComponent({
id: 'camera_preview',
type: 'suraface',
libraryname: ''
})
.controller(this.xComponentController)
.width('100%')
.height('80%')
.onLoad(() => {
// XComponent加载完成后初始化相机
this.initCamera()
})
Row() {
Button('切换相机')
.onClick(() => this.switchCamera())
Button('拍照')
.onClick(() => this.takePhoto())
}
.width('100%')
.padding(20)
}
.width('100%')
.height('100%')
.backgroundColor(Color.Black)
}
}
3. 相机初始化逻辑
这一部分是最容易出问题的地方。CameraManager的创建要放在onLoad回调里,确保XComponent的Surface已经就绪。
typescript
import { camera } from '@kit.CameraKit'
import { common } from '@kit.AbilityKit'
class CameraController {
private cameraManager: camera.CameraManager | null = null
private cameraInput: camera.CameraInput | null = null
private previewOutput: camera.PreviewOutput | null = null
private captureSession: camera.CaptureSession | null = null
async initCamera(context: common.Context, surfaceId: string) {
// 1. 获取CameraManager
this.cameraManager = camera.getCameraManager(context)
if (!this.cameraManager) {
console.error('获取CameraManager失败')
return
}
// 2. 获取相机列表
const cameraArray: Array<camera.CameraDevice> =
await this.cameraManager.getSupportedCameras()
if (cameraArray.length === 0) {
console.error('未找到相机设备')
return
}
// 3. 创建CaptureSession
this.captureSession = await this.cameraManager.createCaptureSession()
// 4. 创建CameraInput
this.cameraInput = await this.cameraManager.createCameraInput(cameraArray[0])
await this.cameraInput.open()
// 5. 创建PreviewOutput
this.previewOutput = await this.cameraManager.createPreviewOutput(surfaceId)
// 6. 配置会话
await this.captureSession.beginConfig()
await this.captureSession.addInput(this.cameraInput)
await this.captureSession.addOutput(this.previewOutput)
await this.captureSession.commitConfig()
// 7. 开始预览
await this.captureSession.start()
}
}
4. 完整的页面代码
把上面两个部分整合到一起,注意处理生命周期和异常。
typescript
import { camera } from '@kit.CameraKit'
import { common } from '@kit.AbilityKit'
@Entry
@Component
struct CameraPreviewPage {
@State isPreviewing: boolean = false
private xComponentController: XComponentController = new XComponentController()
private cameraController: CameraController = new CameraController()
private context = getContext(this) as common.Context
build() {
Column() {
if (!this.isPreviewing) {
Text('正在初始化相机...')
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.width('100%')
.height('80%')
} else {
XComponent({
id: 'camera_preview',
type: 'suraface',
libraryname: ''
})
.controller(this.xComponentController)
.width('100%')
.height('80%')
}
Row() {
Button('切换相机')
.onClick(() => this.switchCamera())
Button('停止预览')
.onClick(() => this.stopPreview())
}
.width('100%')
.padding(20)
}
.width('100%')
.height('100%')
.backgroundColor(Color.Black)
.onPageShow(() => {
// 页面显示时初始化相机
if (!this.isPreviewing) {
this.initCamera()
}
})
.onPageHide(() => {
// 页面隐藏时释放资源
this.releaseCamera()
})
}
async initCamera() {
try {
const surfaceId = this.xComponentController.getXComponentSurfaceId()
await this.cameraController.initCamera(this.context, surfaceId)
this.isPreviewing = true
} catch (error) {
console.error('相机初始化失败:', JSON.stringify(error))
}
}
stopPreview() {
if (this.cameraController.captureSession) {
this.cameraController.captureSession.stop()
}
this.isPreviewing = false
}
releaseCamera() {
if (this.cameraController.cameraInput) {
this.cameraController.cameraInput.close()
}
if (this.cameraController.previewOutput) {
this.cameraController.previewOutput.release()
}
this.isPreviewing = false
}
switchCamera() {
// 切换相机逻辑
}
}
5. 权限申请
在实际设备上运行时,需要在Ability中先申请权限。这里建议使用@ohos.abilityAccessCtrl模块进行运行时授权。
typescript
import { abilityAccessCtrl, common } from '@kit.AbilityKit'
export async function requestCameraPermission(context: common.Context): Promise<boolean> {
const atManager = abilityAccessCtrl.createAtManager()
try {
await atManager.requestPermissionsFromUser(context, [
'ohos.permission.CAMERA',
'ohos.permission.MICROPHONE'
])
return true
} catch (error) {
console.error('相机权限申请失败:', JSON.stringify(error))
return false
}
}
常见问题
坑1:XComponent黑屏问题
现象:XComponent加载完成,但画面一直黑屏。
原因 :这个问题在HarmonyOS开发里比较常见。根本原因是Camera Manager和XComponent Surface的通信时机不对。getXComponentSurfaceId()在onLoad回调之前调用会返回空字符串,导致PreviewOutput无法连接到Surface。
解决方案 :确保在onLoad回调之后调用getXComponentSurfaceId()。推荐的做法是把相机初始化逻辑放在onLoad回调里,而不是onPageShow。
typescript
.onLoad(() => {
// 此时Surface已就绪,可以安全获取ID
const surfaceId = this.xComponentController.getXComponentSurfaceId()
// 初始化相机
this.initCameraWithSurfaceId(surfaceId)
})
坑2:页面返回后预览画面无法恢复
现象:按返回键退出页面,再次进入时预览画面无法显示。
原因 :onPageShow和onPageHide的生命周期管理不完善。很多人只在onPageShow里初始化,但onPageHide时没有正确释放CameraInput和PreviewOutput的资源。第二次进入时,CameraInput还在占用相机设备,新的Session无法创建。
解决方案 :在onPageHide里调用releaseCamera()完全释放资源,在onPageShow里重新初始化。
typescript
.onPageHide(() => {
this.releaseCamera()
})
注意:如果页面跳转使用了路由栈,需要在页面完全销毁时也做一次释放。建议在aboutToDisappear里也调用releaseCamera()。
坑3:权限拒绝后Crash
现象:用户拒绝相机权限后,应用直接崩溃。
原因 :createCameraInput在权限不足时会抛出异常,但很多开发者没有在调用处做try-catch处理。
解决方案:在整个初始化方法外层包裹try-catch,并在权限拒绝时给出友好提示。
typescript
async initCamera() {
try {
const hasPermission = await requestCameraPermission(this.context)
if (!hasPermission) {
// 弹出Dialog提示用户前往设置页面开启权限
return
}
// 继续初始化
} catch (error) {
console.error('初始化异常:', error.code, error.message)
}
}
最佳实践
-
CameraManager推荐做单例处理。每次重新创建CameraManager会有额外开销,而且多个页面同时持有CameraManager实例可能导致设备资源冲突。建议在Application层或页面基类中持有一个全局实例。
-
不要在build()中频繁创建CameraController对象。ArkUI的build()在状态变化时会重绘,如果每次build()都new一个CameraController,会导致之前的CameraInput和PreviewOutput无法被释放。推荐把CameraController作为页面成员变量初始化一次。
-
切换相机时先stop再release再重新配置 。很多人直接调用
captureSession.addInput但忘了旧输入还在,导致Session配置冲突。正确流程是:stop -> release旧Input/Output -> beginConfig -> addInput/addOutput -> commitConfig -> start。
FAQ
Q:为什么真机正常,模拟器不生效?
A:HarmonyOS NEXT模拟器对Camera Kit支持不完整。部分模拟器版本不支持createCameraInput和createPreviewOutput的正常调用。建议只在真机上进行相机相关功能的开发和测试。
Q:为什么页面跳转后预览画面卡住不动?
A:这是典型的生命周期问题。如果页面跳转使用了Navigation,onPageHide和onPageShow的触发时机可能和预期不一致。建议在aboutToAppear和aboutToDisappear中做更严格的资源管理。
Q:为什么第一次授权成功,第二次进入又弹出授权弹窗?
A:检查一下授权请求是在哪个Ability里发起的。如果应用有多个Ability,需要在每个Ability里分别申请权限。推荐在EntryAbility的onCreate或onWindowStageCreate里统一申请一次,后续页面通过checkAccessToken来判断权限状态。