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

《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.CAMERAohos 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的预览数据。这里指定typesurafacelibraryname可以填空字符串。

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:页面返回后预览画面无法恢复

现象:按返回键退出页面,再次进入时预览画面无法显示。

原因onPageShowonPageHide的生命周期管理不完善。很多人只在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)
  }
}

最佳实践

  1. CameraManager推荐做单例处理。每次重新创建CameraManager会有额外开销,而且多个页面同时持有CameraManager实例可能导致设备资源冲突。建议在Application层或页面基类中持有一个全局实例。

  2. 不要在build()中频繁创建CameraController对象。ArkUI的build()在状态变化时会重绘,如果每次build()都new一个CameraController,会导致之前的CameraInput和PreviewOutput无法被释放。推荐把CameraController作为页面成员变量初始化一次。

  3. 切换相机时先stop再release再重新配置 。很多人直接调用captureSession.addInput但忘了旧输入还在,导致Session配置冲突。正确流程是:stop -> release旧Input/Output -> beginConfig -> addInput/addOutput -> commitConfig -> start。

FAQ

Q:为什么真机正常,模拟器不生效?

A:HarmonyOS NEXT模拟器对Camera Kit支持不完整。部分模拟器版本不支持createCameraInputcreatePreviewOutput的正常调用。建议只在真机上进行相机相关功能的开发和测试。

Q:为什么页面跳转后预览画面卡住不动?

A:这是典型的生命周期问题。如果页面跳转使用了Navigation,onPageHideonPageShow的触发时机可能和预期不一致。建议在aboutToAppearaboutToDisappear中做更严格的资源管理。

Q:为什么第一次授权成功,第二次进入又弹出授权弹窗?

A:检查一下授权请求是在哪个Ability里发起的。如果应用有多个Ability,需要在每个Ability里分别申请权限。推荐在EntryAbility的onCreateonWindowStageCreate里统一申请一次,后续页面通过checkAccessToken来判断权限状态。

相关推荐
是摆烂第一名呀1 小时前
RK3576+GMSL摄像头底层适配与调试
linux·arm开发·驱动开发·嵌入式硬件·数码相机
youtootech1 小时前
HarmonyOS《柚兔学伴》项目实战02-多模块架构设计与 HSP/HAR 模块化
华为·harmonyos
不才难以繁此生1 小时前
ArkUI Tabs 切换后状态丢失怎么办:中式美食分类页怎么保留关键词、筛选和滚动位置
harmonyos·状态管理·arkui·tabs·中式美食
wuhanzhanhui12 小时前
从 3D 建模到高光谱2026 武汉机器视觉展会|工业相机展会洞见感知力量新高度
数码相机·3d
爱吃大芒果15 小时前
复刻 HarmonyKit 工具箱:基于新版 @kit 开发本地工具 App
华为·harmonyos
风华圆舞15 小时前
鸿蒙上跑通第一个 AI Agent:5 分钟接入智谱/DeepSeek
人工智能·华为·harmonyos·arkts·arkui·toolregistry
3DVisionary16 小时前
单目高速DIC如何啃下精密件微米级高频振动这块“硬骨头“
数码相机·航空发动机微型叶片·mems器件·pcb与电子封装·精密钟表/齿轮·微纳加工件·全尺寸3d检测
xd18557855516 小时前
电影匹配引擎-基于鸿蒙的心情电影推荐应用开发实践
人工智能·安全·华为·harmonyos·鸿蒙
千逐6817 小时前
HarmonyOS 实战 | @State 不神秘——变量一变界面就跟着变
华为·harmonyos·鸿蒙