相机预览功能全面实现

96 阅读3分钟

相机预览功能全面实现

1. 预览功能概述

相机预览是相机应用中最基本也是最重要的功能,它让用户能够实时看到相机捕捉的画面。在鸿蒙系统中,预览功能主要通过XComponent组件和Camera Kit的PreviewOutput来实现。

1.1 预览流程

  1. 创建XComponent组件,获取SurfaceId。
  2. 通过CameraManager创建PreviewOutput,传入SurfaceId。
  3. 创建相机会话(Session),将CameraInput和PreviewOutput添加到会话中。
  4. 配置并启动会话,开始预览。

1.2 关键组件

  • XComponent: 用于显示相机预览画面的组件,提供Surface。
  • PreviewOutput: 相机预览输出流,将相机数据渲染到XComponent的Surface上。

2. XComponent与Surface集成

2.1 创建XComponent

在ArkUI中,我们使用XComponent来显示相机预览。XComponent提供了Surface,相机框架将预览数据渲染到这个Surface上。

typescript

import { XComponentController, XComponent, XComponentType } from '@ohos.arkui.xcomponent';
import { UIContext } from '@ohos.arkui.UIContext';

@Entry
@Component
struct CameraPreview {
  private xComponentController: XComponentController = new XComponentController();
  private surfaceId: string = '';
  // 预览流的宽高,根据相机支持的profile设置
  @State previewWidth: number = 1920;
  @State previewHeight: number = 1080;
  private uiContext: UIContext = getUIContext();

  build() {
    Column() {
      // XComponent用于显示预览画面
      XComponent({
        id: 'xcomponent_camera_preview',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      })
        .onLoad(() => {
          // 获取SurfaceId
          this.surfaceId = this.xComponentController.getXComponentSurfaceId();
          console.info(`XComponent SurfaceId: ${this.surfaceId}`);
          // 初始化相机预览
          this.initCameraPreview();
        })
        .width(this.uiContext.px2vp(this.previewWidth))
        .height(this.uiContext.px2vp(this.previewHeight))
        .backgroundColor(Color.Black)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Black)
  }

  async initCameraPreview() {
    // 初始化相机预览,具体实现见后续章节
  }
}

2.2 设置XComponent的宽高

XComponent的宽高设置需要与相机预览流的宽高比例一致,否则预览画面会被拉伸。通常,我们会根据相机支持的预览配置(Profile)来设置XComponent的宽高。

3. 预览流配置

3.1 创建PreviewOutput

在获取到SurfaceId后,我们需要创建PreviewOutput。首先,我们需要从相机的输出能力(CameraOutputCapability)中获取预览配置(Profile),然后创建PreviewOutput。

typescript

import { camera } from '@kit.CameraKit';
import { BusinessError } from '@kit.BasicServicesKit';

class PreviewManager {
  private previewOutput: camera.PreviewOutput | undefined;

  // 创建预览输出流
  createPreviewOutput(
    cameraManager: camera.CameraManager,
    capability: camera.CameraOutputCapability,
    surfaceId: string
  ): camera.PreviewOutput | undefined {
    // 从能力中获取预览配置,这里选择第一个,实际应根据业务需求选择
    let previewProfiles: Array<camera.Profile> = capability.previewProfiles;
    if (previewProfiles.length === 0) {
      console.error('No preview profiles supported.');
      return undefined;
    }

    // 选择预览配置,这里选择第一个,实际中应根据分辨率、格式等选择最合适的
    let previewProfile: camera.Profile = previewProfiles[0];

    try {
      this.previewOutput = cameraManager.createPreviewOutput(previewProfile, surfaceId);
      console.info('PreviewOutput created successfully.');
      return this.previewOutput;
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to create PreviewOutput, error code: ${err.code}`);
      return undefined;
    }
  }

  // 释放预览输出流
  async releasePreviewOutput(): Promise<void> {
    if (this.previewOutput) {
      try {
        await this.previewOutput.release();
        this.previewOutput = undefined;
        console.info('PreviewOutput released successfully.');
      } catch (error) {
        let err = error as BusinessError;
        console.error(`Failed to release PreviewOutput, error code: ${err.code}`);
      }
    }
  }
}

3.2 配置相机会话

创建PreviewOutput后,我们需要创建相机会话,并将CameraInput和PreviewOutput添加到会话中。

typescript

class CameraSessionManager {
  private session: camera.PhotoSession | undefined;

  // 创建会话并配置输入输出
  async createAndConfigSession(
    cameraManager: camera.CameraManager,
    cameraInput: camera.CameraInput,
    previewOutput: camera.PreviewOutput
  ): Promise<boolean> {
    try {
      // 创建拍照模式的会话
      this.session = cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;

      // 开始配置
      this.session.beginConfig();

      // 添加输入
      this.session.addInput(cameraInput);

      // 添加预览输出
      this.session.addOutput(previewOutput);

      // 提交配置
      await this.session.commitConfig();

      // 启动会话
      await this.session.start();

      console.info('Session started successfully.');
      return true;
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Failed to create and config session, error code: ${err.code}`);
      return false;
    }
  }

  // 停止并释放会话
  async releaseSession(): Promise<void> {
    if (this.session) {
      try {
        await this.session.stop();
        await this.session.release();
        this.session = undefined;
        console.info('Session released successfully.');
      } catch (error) {
        let err = error as BusinessError;
        console.error(`Failed to release session, error code: ${err.code}`);
      }
    }
  }
}

4. 多路预览实现

在某些场景下,我们可能需要同时使用两路预览流,一路用于显示,一路用于图像处理(如人脸识别、美颜等)。鸿蒙相机框架支持同时添加多个PreviewOutput。

4.1 双路预览的实现步骤

  1. 创建两个XComponent,获取两个SurfaceId。
  2. 创建两个PreviewOutput,分别绑定两个SurfaceId。
  3. 将两个PreviewOutput都添加到会话中。

4.2 双路预览代码示例

typescript

// 在UI中创建两个XComponent
@Entry
@Component
struct DualCameraPreview {
  private xComponentController1: XComponentController = new XComponentController();
  private xComponentController2: XComponentController = new XComponentController();
  private surfaceId1: string = '';
  private surfaceId2: string = '';
  @State previewWidth: number = 1920;
  @State previewHeight: number = 1080;
  private uiContext: UIContext = getUIContext();

  build() {
    Column() {
      // 第一个XComponent用于显示预览画面
      XComponent({
        id: 'xcomponent_camera_preview1',
        type: XComponentType.SURFACE,
        controller: this.xComponentController1
      })
        .onLoad(() => {
          this.surfaceId1 = this.xComponentController1.getXComponentSurfaceId();
          console.info(`XComponent1 SurfaceId: ${this.surfaceId1}`);
        })
        .width(this.uiContext.px2vp(this.previewWidth))
        .height(this.uiContext.px2vp(this.previewHeight))
        .backgroundColor(Color.Black)

      // 第二个XComponent用于图像处理预览
      XComponent({
        id: 'xcomponent_camera_preview2',
        type: XComponentType.SURFACE,
        controller: this.xComponentController2
      })
        .onLoad(() => {
          this.surfaceId2 = this.xComponentController2.getXComponentSurfaceId();
          console.info(`XComponent2 SurfaceId: ${this.surfaceId2}`);
          // 当两个Surface都准备好后,初始化相机
          if (this.surfaceId1 && this.surfaceId2) {
            this.initDualPreview();
          }
        })
        .width(this.uiContext.px2vp(this.previewWidth))
        .height(this.uiContext.px2vp(this.previewHeight))
        .backgroundColor(Color.Black)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Black)
  }

  async initDualPreview() {
    // 初始化双路预览
    // 1. 创建CameraManager和CameraInput(略)
    // 2. 创建两个PreviewOutput
    let previewOutput1 = previewManager1.createPreviewOutput(cameraManager, capability, this.surfaceId1);
    let previewOutput2 = previewManager2.createPreviewOutput(cameraManager, capability, this.surfaceId2);

    // 3. 创建会话,并同时添加两个PreviewOutput
    let sessionManager = new CameraSessionManager();
    await sessionManager.createAndConfigSession(cameraManager, cameraInput, [previewOutput1, previewOutput2]);
  }
}

注意:在添加多个输出流时,需要确保相机设备支持多路流。同时,多路流可能会占用更多的系统资源,需要根据设备性能进行调整。

5. 预览状态监控与错误处理

5.1 预览状态监听

我们可以监听PreviewOutput的状态,以便了解预览的开始、结束和错误情况。

typescript

class PreviewListener {
  setupPreviewListeners(previewOutput: camera.PreviewOutput): void {
    // 监听预览开始(第一帧曝光)
    previewOutput.on('frameStart', (err: BusinessError) => {
      if (err) {
        console.error(`Preview frameStart error: ${err.code}`);
        return;
      }
      console.info('Preview frame started');
    });

    // 监听预览结束(最后一帧)
    previewOutput.on('frameEnd', (err: BusinessError) => {
      if (err) {
        console.error(`Preview frameEnd error: ${err.code}`);
        return;
      }
      console.info('Preview frame ended');
    });

    // 监听预览错误
    previewOutput.on('error', (err: BusinessError) => {
      console.error(`Preview error: ${err.code}`);
    });
  }
}

5.2 错误处理

在预览过程中,可能会遇到各种错误,如Surface不可用、相机设备断开等。我们需要监听这些错误并做出相应处理。

typescript

class CameraErrorHandler {
  static handlePreviewError(error: BusinessError): void {
    console.error(`Preview error occurred: code=${error.code}, message=${error.message}`);
    // 根据错误码进行相应处理
    switch (error.code) {
      case 5400101:
        // 设备未找到,可能相机被拔出
        break;
      case 5400102:
        // 设备被占用,提示用户
        break;
      case 5400103:
        // 配置失败,尝试重新配置
        break;
      default:
        // 其他错误
        break;
    }
  }
}

6. 性能优化

6.1 选择合适的预览配置

选择预览配置时,应考虑以下因素:

  • 分辨率:高分辨率会消耗更多资源,选择与显示区域匹配的分辨率。
  • 格式:常用的格式有YUV和RGBA,根据需求选择。
  • 帧率:根据业务需求选择适当的帧率。

6.2 资源释放

在页面消失或不再需要预览时,及时释放资源,包括PreviewOutput和Session。

typescript

async onPageHide(): Promise<void> {
  await this.previewManager.releasePreviewOutput();
  await this.sessionManager.releaseSession();
}

7. 完整示例

以下是一个完整的相机预览示例,整合了以上内容:

typescript

import { camera } from '@kit.CameraKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { abilityAccessCtrl, Permissions, common } from '@kit.AbilityKit';

@Entry
@Component
struct CameraPreviewPage {
  private xComponentController: XComponentController = new XComponentController();
  private surfaceId: string = '';
  @State previewWidth: number = 1920;
  @State previewHeight: number = 1080;
  private uiContext: UIContext = getUIContext();
  private context: common.BaseContext = getContext() as common.BaseContext;

  // 相机相关对象
  private cameraManager: camera.CameraManager | undefined;
  private cameraInput: camera.CameraInput | undefined;
  private previewOutput: camera.PreviewOutput | undefined;
  private session: camera.PhotoSession | undefined;

  async onPageShow(): Promise<void> {
    // 申请权限
    let atManager = abilityAccessCtrl.createAtManager();
    let permissions: Array<Permissions> = ['ohos.permission.CAMERA'];
    let result = await atManager.requestPermissionsFromUser(this.context, permissions);
    if (result.authResults[0] === 0) {
      this.initCamera();
    } else {
      console.error('Camera permission denied');
    }
  }

  async initCamera(): Promise<void> {
    try {
      // 获取CameraManager
      this.cameraManager = camera.getCameraManager(this.context);
      // 获取相机设备
      let cameras: Array<camera.CameraDevice> = this.cameraManager.getSupportedCameras();
      if (cameras.length === 0) {
        console.error('No cameras found');
        return;
      }
      let cameraDevice = cameras[0];

      // 创建CameraInput
      this.cameraInput = this.cameraManager.createCameraInput(cameraDevice);
      await this.cameraInput.open();

      // 获取输出能力
      let capability = this.cameraManager.getSupportedOutputCapability(cameraDevice, camera.SceneMode.NORMAL_PHOTO);
      if (!capability) {
        console.error('Failed to get output capability');
        return;
      }

      // 创建PreviewOutput
      let previewProfiles = capability.previewProfiles;
      if (previewProfiles.length === 0) {
        console.error('No preview profiles');
        return;
      }
      let previewProfile = previewProfiles[0];
      this.previewOutput = this.cameraManager.createPreviewOutput(previewProfile, this.surfaceId);

      // 设置预览监听
      this.setupPreviewListeners();

      // 创建会话
      this.session = this.cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
      this.session.beginConfig();
      this.session.addInput(this.cameraInput);
      this.session.addOutput(this.previewOutput);
      await this.session.commitConfig();
      await this.session.start();

    } catch (error) {
      let err = error as BusinessError;
      console.error(`Camera initialization failed: ${err.code}`);
    }
  }

  setupPreviewListeners(): void {
    if (!this.previewOutput) return;

    this.previewOutput.on('frameStart', (err: BusinessError) => {
      if (err) {
        console.error(`frameStart error: ${err.code}`);
        return;
      }
      console.info('Preview frame started');
    });

    this.previewOutput.on('frameEnd', (err: BusinessError) => {
      if (err) {
        console.error(`frameEnd error: ${err.code}`);
        return;
      }
      console.info('Preview frame ended');
    });

    this.previewOutput.on('error', (err: BusinessError) => {
      console.error(`Preview error: ${err.code}`);
    });
  }

  async onPageHide(): Promise<void> {
    // 释放资源
    if (this.session) {
      await this.session.stop();
      await this.session.release();
    }
    if (this.cameraInput) {
      await this.cameraInput.close();
    }
    if (this.previewOutput) {
      await this.previewOutput.release();
    }
  }

  build() {
    Column() {
      XComponent({
        id: 'xcomponent_camera_preview',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      })
        .onLoad(() => {
          this.surfaceId = this.xComponentController.getXComponentSurfaceId();
          console.info(`SurfaceId: ${this.surfaceId}`);
        })
        .width(this.uiContext.px2vp(this.previewWidth))
        .height(this.uiContext.px2vp(this.previewHeight))
        .backgroundColor(Color.Black)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Black)
  }
}

总结

本篇详细介绍了鸿蒙相机预览功能的实现,包括XComponent的创建、PreviewOutput的配置、相机会话的管理、双路预览的实现以及状态监控和错误处理。通过以上步骤,开发者可以实现一个完整的相机预览功能。