前言

在移动互联网时代,相机早已超越了单一的“拍照工具”范畴,深度渗透到扫码支付、人脸识别、AR 互动及内容创作等核心场景。对于开发者而言,在应用内实现一个功能完备、画面流畅且 UI 高度定制的相机功能,面临着硬件生命周期管理、多分辨率适配以及性能平衡等多重挑战。

鸿蒙 HarmonyOS 6 (API 20) 中,Camera Kit 引入了一套基于 Session 会话机制 的全新开发范式。它将复杂的硬件操作抽象为清晰的 输入流 (Input)会话管理 (Session)输出流 (Output),极大地降低了开发门槛。

本文将脱离简单的 Intent 跳转,深入底层,手把手带你构建一个支持预览、拍照和录像的自定义相机应用。

一、 流水线思维:输入、会话与输出

驾驭 Camera Kit 的关键在于建立 流水线 思维模型。在 API 20 中,相机的运作是一个完整的数据流动过程,可类比为摄影棚工作流:

  • 输入流 (CameraInput):相当于摄影师,负责采集光影信号。
  • 会话 (CaptureSession):相当于导演,控制全场调度(启动/停止/配置)。
  • 输出流 (Output):相当于不同的终端设备。
    • 预览输出 (PreviewOutput):送往屏幕显示(监视器)。
    • 拍照输出 (PhotoOutput):送往图像处理引擎生成图片(冲印室)。
    • 录像输出 (VideoOutput):送往编码器生成视频文件(录像机)。

开发流程通常始于 camera.getCameraManager。通过相机管理器,我们查询设备支持的相机列表(前置/后置)及其能力集(分辨率/帧率)。选择合适的相机设备后,创建 Input 和 Session,并将 Output 像积木一样组装起来。

值得注意的是,Session 是核心枢纽,所有配置修改(如变焦、闪光灯)必须在 Session.commitConfig() 后才能生效。

二、 预览流渲染:XComponent 与 SurfaceId 的羁绊

预览是相机开发的第一道难关,因为它要求实时、高帧率地渲染。普通的 UI 组件无法通过性能瓶颈,鸿蒙为此提供了 XComponent

  • XComponent:专为高性能渲染设计,提供底层渲染表面 (Surface),允许硬件直接写入显存,绕过 UI 层冗余。
  • SurfaceId:连接相机与屏幕的纽带。
    1. 在 UI 中放置 XComponent
    2. 监听 onLoad 回调,获取唯一的 surfaceId
    3. 调用 cameraManager.createPreviewOutput 时传入该 ID。

若 ID 传递错误或时序颠倒,预览画面将呈现黑屏。

三、 捕捉光影:拍照与录像实现细节

1. 拍照 (PhotoOutput)

创建 PhotoOutput 时,需通过 PhotoProfile 指定分辨率。通常策略是遍历设备能力集,选择满足需求(如最高像素)的配置。调用 capture() 方法触发快门时,可传入单次拍摄参数(如地理位置、镜像)。

2. 录像 (VideoOutput)

录像实现更为复杂,涉及音频录制、视频编码及文件封装。在鸿蒙中,VideoOutput 需配合 AVRecorder 模块:

  1. 初始化 AVRecorder,配置编码格式 (H.264/H.265)、音频采样率等。
  2. AVRecorder 获取输入 Surface 的 ID。
  3. 将此 ID 传给 VideoOutput,建立数据通路。

注意:录像涉及底层硬件编码器资源,释放逻辑 (release) 必须严谨,否则极易导致后续录像失败或相机卡死。

四、 实战代码示例

以下代码封装了一个 CameraService 单例类,完整展示了从 获取权限 -> 初始化相机 -> 绑定 XComponent -> 启动预览 -> 实现拍照 -> 资源释放 的全流程。

import { camera } from '@kit.CameraKit';
import { image } from '@kit.ImageKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { promptAction } from '@kit.ArkUI';

// -------------------------------------------------------------
// 1. 相机服务封装类 (核心逻辑)
// -------------------------------------------------------------
class CameraService {
  private cameraManager: camera.CameraManager | null = null;
  private cameraInput: camera.CameraInput | null = null;
  private captureSession: camera.PhotoSession | null = null; // API 20 推荐使用 PhotoSession
  private previewOutput: camera.PreviewOutput | null = null;
  private photoOutput: camera.PhotoOutput | null = null;
  
  // 当前的 SurfaceId,由 XComponent 提供
  private surfaceId: string = '';

  /**
   * 初始化相机管理器
   */
  init(context: common.Context) {
    if (!this.cameraManager) {
      this.cameraManager = camera.getCameraManager(context);
    }
  }

  /**
   * 启动相机预览
   * @param surfaceId XComponent 提供的渲染表面 ID
   */
  async startPreview(surfaceId: string) {
    this.surfaceId = surfaceId;
    if (!this.cameraManager) return;

    try {
      // 1. 获取支持的相机设备列表
      const cameras = this.cameraManager.getSupportedCameras();
      if (cameras.length === 0) {
        console.error('[Camera] No camera devices found');
        return;
      }
      // 默认选择第一个相机 (通常是后置主摄)
      const cameraDevice = cameras[0];

      // 2. 创建相机输入流 (Input)
      this.cameraInput = this.cameraManager.createCameraInput(cameraDevice);
      await this.cameraInput.open();

      // 3. 获取相机能力集,选择合适的配置 (Profile)
      const capability = this.cameraManager.getSupportedOutputCapability(cameraDevice, camera.SceneMode.NORMAL_PHOTO);
      // 简单起见,选择预览流的第一个配置
      const previewProfile = capability.previewProfiles[0];
      // 选择拍照流的第一个配置 (实际开发应筛选最高分辨率)
      const photoProfile = capability.photoProfiles[0];

      // 4. 创建输出流 (Output)
      // 预览输出:绑定到 XComponent 的 surfaceId
      this.previewOutput = this.cameraManager.createPreviewOutput(previewProfile, this.surfaceId);
      // 拍照输出
      this.photoOutput = this.cameraManager.createPhotoOutput(photoProfile);

      // 5. 创建会话 (Session) 并组装
      this.captureSession = this.cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
      
      this.captureSession.beginConfig();
      this.captureSession.addInput(this.cameraInput);
      this.captureSession.addOutput(this.previewOutput);
      this.captureSession.addOutput(this.photoOutput);
      
      await this.captureSession.commitConfig();
      await this.captureSession.start();

      console.info('[Camera] Preview started successfully');

    } catch (err) {
      const error = err as BusinessError;
      console.error(`[Camera] Failed to start preview: ${error.message}`);
    }
  }

  /**
   * 拍照功能
   */
  async takePhoto() {
    if (!this.photoOutput) return;

    try {
      // 配置拍照参数
      const photoCaptureSetting: camera.PhotoCaptureSetting = {
        quality: camera.QualityLevel.QUALITY_LEVEL_HIGH,
        rotation: camera.ImageRotation.ROTATION_0
      };

      // 触发拍照
      await this.photoOutput.capture(photoCaptureSetting);
      
      // 注意:PhotoOutput.capture 只是触发动作
      // 实际获取图片数据通常需要监听 'photoAvailable' 事件并配合 PhotoAccessHelper 保存
      // 这里仅做触发演示
      promptAction.showToast({ message: '咔嚓!拍照触发成功' });

    } catch (err) {
      console.error(`[Camera] Take photo failed: ${(err as BusinessError).message}`);
    }
  }

  /**
   * 释放资源
   * 必须在页面销毁时调用,否则可能导致相机无法再次打开
   */
  async release() {
    console.info('[Camera] Releasing resources...');
    try {
      await this.captureSession?.stop();
      await this.captureSession?.release();
      await this.cameraInput?.close();
      await this.previewOutput?.release();
      await this.photoOutput?.release();
    } catch (err) {
      console.error('[Camera] Release failed', err);
    } finally {
      this.captureSession = null;
      this.cameraInput = null;
      this.previewOutput = null;
      this.photoOutput = null;
    }
  }
}

// 导出单例
const cameraService = new CameraService();


// -------------------------------------------------------------
// 2. 相机预览与交互页面
// -------------------------------------------------------------
@Entry
@Component
struct CameraPage {
  private xComponentController: XComponentController = new XComponentController();
  // 标记 XComponent 是否加载完成
  @State isSurfaceReady: boolean = false;

  aboutToAppear(): void {
    const context = getContext(this) as common.UIAbilityContext;
    cameraService.init(context);
  }

  aboutToDisappear(): void {
    // 页面销毁时务必释放相机资源
    cameraService.release();
  }

  build() {
    Stack() {
      // 1. 相机预览区域
      // 使用 XComponent 承载预览流
      XComponent({
        id: 'cameraPreview',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      })
        .onLoad(() => {
          // 核心:当 XComponent 加载完成,获取 surfaceId
          this.xComponentController.setXComponentSurfaceSize({ surfaceWidth: 1080, surfaceHeight: 1920 });
          const surfaceId = this.xComponentController.getXComponentSurfaceId();
          console.info(`[UI] Surface created: ${surfaceId}`);
          
          this.isSurfaceReady = true;
          // 启动预览
          cameraService.startPreview(surfaceId);
        })
        .width('100%')
        .height('100%')

      // 2. 拍照控制层 (覆盖在预览之上)
      Column() {
        // 顶部工具栏 (模拟)
        Row() {
          // 仅作为 UI 占位示意
          Text('Flash').fontColor(Color.White).fontSize(14)
          Text('HDR').fontColor(Color.White).fontSize(14)
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .padding({ top: 40, left: 20, right: 20 })

        // 底部拍照按钮
        Blank() // 占位,把按钮顶到底部

        Row() {
          // 拍照快门键
          Button()
            .width(80)
            .height(80)
            .borderRadius(40)
            .backgroundColor(Color.White)
            .border({ width: 4, color: '#CCCCCC' })
            .onClick(() => {
              cameraService.takePhoto();
            })
            // 添加一个按压效果动画
            .stateEffect(true) 
        }
        .width('100%')
        .justifyContent(FlexAlign.Center)
        .padding({ bottom: 50 })
      }
      .width('100%')
      .height('100%')
      // 让点击事件穿透到下层 (除了按钮本身)
      .hitTestBehavior(HitTestMode.Transparent) 
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Black) // 相机加载前显示黑色背景
  }
}

五、 总结

自定义相机开发是鸿蒙多媒体开发中极具挑战也极具价值的领域。通过本文,我们掌握了:

  1. Session 范式:理解 Input -> Session -> Output 的数据流转模型。
  2. 渲染机制:利用 XComponentSurfaceId 实现高性能预览。
  3. 资源管理:严格遵循生命周期,正确创建与释放相机资源。

一旦掌握了这套基础架构,你便可以在此基础上扩展出更丰富的功能,如扫码识别、AR 特效叠加或专业模式摄影,为用户提供极致的视觉体验。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐