预览(ArkTS)
=========

在开发相机应用时，需要先[申请相关权限](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/camera-preparation)。

预览是启动相机后看见的画面，通常在拍照和录像前执行。

#### 开发步骤

详细的API说明请参考[@ohos.multimedia.camera (相机管理)](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera)。

1.  导入camera接口，接口中提供了相机相关的属性和方法，导入方法如下。
    
    import { camera } from '@kit.CameraKit';
    import { BusinessError } from '@kit.BasicServicesKit';
    
2.  创建Surface。
    
    相机开发模型为Surface模型，该模型主要通过Surface实现数据交互。在开发相机应用界面时，首先需要通过创建XComponent组件为预览流提供Surface，再通过获取XComponent组件对应Surface的ID创建预览流，预览流画面即可直接在XComponent组件内渲染，详细获取surfaceId请参考[getXComponentSurfaceId](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-xcomponent#getxcomponentsurfaceid9)方法。而XComponent的能力由UI提供，相关介绍可参考[XComponent组件参考](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-xcomponent)。
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/c3/v3/hjqu2sGrSmOGTQIZogMpQg/note_3.0-zh-cn.png?HW-CC-KV=V1&HW-CC-Date=20260612T073512Z&HW-CC-Expire=86400&HW-CC-Sign=418086939831ABEF16BC05DF9C16A98B2FE707EA0FEF4B0161EF3CD2489C7912)
    
    预览流与录像输出流的分辨率的宽高比要保持一致，如果设置XComponent组件中的Surface显示区域宽高比为1920:1080 = 16:9，则需要预览流中的分辨率的宽高比也为16:9，如分辨率选择640:360，或960:540，或1920:1080，以此类推。
    
    @Entry
    @Component
    struct example {
      xComponentCtl: XComponentController = new XComponentController();
      surfaceId:string = '';
      imageWidth: number = 1920;
      imageHeight: number = 1080;
      private uiContext: UIContext = this.getUIContext();
      private mXComponentOptions: XComponentOptions = {
        type: XComponentType.SURFACE,
        controller: this.xComponentCtl
      }
    
      build() {
        XComponent(this.mXComponentOptions)
          .onLoad(async () => {
            console.info('onLoad is called');
            this.surfaceId = this.xComponentCtl.getXComponentSurfaceId(); // 获取组件surfaceId。
            // 使用surfaceId创建预览流，开启相机，组件实时渲染每帧预览流数据。
          })
          // surface的宽、高设置与XComponent组件的宽、高设置相反，或使用.renderFit(RenderFit.RESIZE\_CONTAIN)自动填充显示无需设置宽、高。
          .width(this.uiContext.px2vp(this.imageHeight))
          .height(this.uiContext.px2vp(this.imageWidth))
      }
    }
    
3.  通过[CameraOutputCapability](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-i#cameraoutputcapability)中的previewProfiles属性获取当前设备支持的预览能力，返回previewProfilesArray数组 。通过[createPreviewOutput](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-cameramanager#createpreviewoutput)方法创建预览输出流，其中，[createPreviewOutput](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-cameramanager#createpreviewoutput)方法中的两个参数分别是当前设备支持的预览配置信息previewProfile和步骤二中获取的surfaceId。
    
    function getPreviewOutput(cameraManager: camera.CameraManager, cameraOutputCapability: camera.CameraOutputCapability, surfaceId: string): camera.PreviewOutput | undefined {
      if (!cameraOutputCapability || !cameraOutputCapability.previewProfiles) {
        return;
      }
      let previewProfilesArray: Array<camera.Profile> = cameraOutputCapability.previewProfiles;
      if (!previewProfilesArray || previewProfilesArray.length === 0) {
        console.error("previewProfilesArray is null or \[\]");
        return;
      }
      let previewOutput: camera.PreviewOutput | undefined = undefined;
      try {
        // previewProfilesArray要选择与步骤二设置宽高比一致的previewProfile配置信息，此处选择数组第一项仅供接口使用示例参考。
        previewOutput = cameraManager.createPreviewOutput(previewProfilesArray\[0\], surfaceId);
      } catch (error) {
        let err = error as BusinessError;
        console.error("Failed to create the PreviewOutput instance. error code: " + err.code);
      }
      return previewOutput;
    }
    
4.  使能。通过[Session.start](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-session#start11)方法输出预览流，接口调用失败会返回相应错误码，错误码类型参见[CameraErrorCode](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-e#cameraerrorcode)。
    
    async function startPreviewOutput(cameraManager: camera.CameraManager, previewOutput: camera.PreviewOutput): Promise<void> {
      try {
        let cameraArray: Array<camera.CameraDevice> = \[\];
        cameraArray = cameraManager.getSupportedCameras();
        if (cameraArray.length == 0) {
          console.error('no camera.');
          return;
        }
        // 获取支持的模式类型。
        let sceneModes: Array<camera.SceneMode> = cameraManager.getSupportedSceneModes(cameraArray\[0\]);
        let isSupportPhotoMode: boolean = sceneModes.indexOf(camera.SceneMode.NORMAL\_PHOTO) >= 0;
        if (!isSupportPhotoMode) {
          console.error('photo mode not support');
          return;
        }
        let cameraInput: camera.CameraInput | undefined;
        cameraInput = cameraManager.createCameraInput(cameraArray\[0\]);
        if (cameraInput === undefined) {
          console.error('cameraInput is undefined');
          return;
        }
        // 打开相机。
        await cameraInput.open();
        let session = cameraManager.createSession(camera.SceneMode.NORMAL\_PHOTO);
        if (!session) {
          console.error('session is null');
          return;
        }
        let photoSession: camera.PhotoSession = session as camera.PhotoSession;
        photoSession.beginConfig();
        photoSession.addInput(cameraInput);
        photoSession.addOutput(previewOutput);
        await photoSession.commitConfig();
        await photoSession.start();
      } catch (error) {
        console.error(\`startPreviewOutput call failed, error: ${error}\`);
      }
    }
    

#### 状态监听

在相机应用开发过程中，可以随时监听预览输出流状态，包括预览流启动、预览流结束、预览流输出错误。

*   通过注册固定的[on('frameStart')](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-previewoutput#onframestart)回调函数获取监听预览启动结果，previewOutput创建成功时即可监听，预览第一次曝光时触发，有该事件返回结果则认为预览流已启动。
    
    function onPreviewOutputFrameStart(previewOutput: camera.PreviewOutput): void {
      previewOutput.on('frameStart', (err: BusinessError) => {
        if (err !== undefined && err.code !== 0) {
          return;
        }
        console.info('Preview frame started');
      });
    }
    
*   通过注册固定的[on('frameEnd')](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-previewoutput#onframeend)回调函数获取监听预览结束结果，previewOutput创建成功时即可监听，预览完成最后一帧时触发，有该事件返回结果则认为预览流已结束。
    
    function onPreviewOutputFrameEnd(previewOutput: camera.PreviewOutput): void {
      previewOutput.on('frameEnd', (err: BusinessError) => {
        if (err !== undefined && err.code !== 0) {
          return;
        }
        console.info('Preview frame ended');
      });
    }
    
*   通过注册固定的error回调函数获取监听预览输出错误结果，回调返回预览输出接口使用错误时对应的错误码，错误码类型参见[CameraErrorCode](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-e#cameraerrorcode)。
    
    function onPreviewOutputError(previewOutput: camera.PreviewOutput): void {
      previewOutput.on('error', (previewOutputError: BusinessError) => {
        console.error(\`Preview output error code: ${previewOutputError.code}\`);
      });
    }
    

#### 完整示例

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


@Entry
@Component
struct Index {
  private xComponentCtl: XComponentController = new XComponentController();
  private xComponentSurfaceId: string = '';
  @State imageWidth: number = 1920;
  @State imageHeight: number = 1080;
  private cameraManager: camera.CameraManager | undefined = undefined;
  private cameras: Array<camera.CameraDevice> | undefined = \[\];
  private cameraInput: camera.CameraInput | undefined = undefined;
  private previewOutput: camera.PreviewOutput | undefined = undefined;
  private session: camera.VideoSession | undefined = undefined;
  private uiContext: UIContext = this.getUIContext();
  private context: Context | undefined = this.uiContext.getHostContext();
  private cameraPermission: Permissions = 'ohos.permission.CAMERA'; // 申请权限相关问题可参考本篇开头的申请相关权限文档
  @State isShow: boolean = false;
  private mXComponentOptions: XComponentOptions = {
    type: XComponentType.SURFACE,
    controller: this.xComponentCtl
  }

  async requestPermissionsFn(): Promise<void> {
    let atManager = abilityAccessCtrl.createAtManager();
    if (this.context) {
      let res = await atManager.requestPermissionsFromUser(this.context, \[this.cameraPermission\]);
      for (let i = 0; i < res.permissions.length; i++) {
        if (this.cameraPermission.toString() === res.permissions\[i\] && res.authResults\[i\] === 0) {
          this.isShow = true;
        }
      }
    }
  }

  aboutToAppear(): void {
    this.requestPermissionsFn();
  }

  onPageShow(): void {
    console.info('onPageShow');
    if (this.xComponentSurfaceId !== '') {
      this.initCamera();
    }
  }

  onPageHide(): void {
    console.info('onPageHide');
    this.releaseCamera();
  }

  build() {
    Column() {
      if (this.isShow) {
        XComponent(this.mXComponentOptions)
          .onLoad(async () => {
            console.info('onLoad is called');
            this.xComponentSurfaceId = this.xComponentCtl.getXComponentSurfaceId(); // 获取组件surfaceId。
            // 初始化相机，组件实时渲染每帧预览流数据。
            this.initCamera()
          })
          .width(this.uiContext.px2vp(this.imageHeight))
          .height(this.uiContext.px2vp(this.imageWidth))
      }
    }
    .justifyContent(FlexAlign.Center)
    .height('100%')
    .width('100%')
  }


  // 初始化相机。
  async initCamera(): Promise<void> {
    console.info(\`initCamera previewOutput xComponentSurfaceId:${this.xComponentSurfaceId}\`);
    try {
      // 获取相机管理器实例。
      this.cameraManager = camera.getCameraManager(this.context);
      if (!this.cameraManager) {
        console.error('initCamera getCameraManager');
        return;
      }
      // 获取当前设备支持的相机device列表。
      this.cameras = this.cameraManager.getSupportedCameras();
      if (!this.cameras) {
        console.error('initCamera getSupportedCameras');
      }
      // 选择一个相机device，创建cameraInput输出对象。
      this.cameraInput = this.cameraManager.createCameraInput(this.cameras\[0\]);
      if (!this.cameraInput) {
        console.error('initCamera createCameraInput');
        return;
      }
      // 打开相机。
      await this.cameraInput.open();
      // 获取相机device支持的profile。
      let capability: camera.CameraOutputCapability =
        this.cameraManager.getSupportedOutputCapability(this.cameras\[0\], camera.SceneMode.NORMAL\_VIDEO);
      if (!capability || capability.previewProfiles.length === 0) {
        console.error('capability is null || \[\]');
        this.releaseCamera();
        return;
      }
      let minRatioDiff : number = 0.1;
      let surfaceRatio : number = this.imageWidth / this.imageHeight; // 最接近16:9宽高比。
      let previewProfile: camera.Profile = capability.previewProfiles\[0\];
      // 应用开发者根据实际业务需求选择一个支持的预览流previewProfile。
      // 此处以选择CAMERA\_FORMAT\_YUV\_420\_SP（NV21）格式、满足限定条件分辨率的预览流previewProfile为例。
      for (let index = 0; index < capability.previewProfiles.length; index++) {
        const tempProfile = capability.previewProfiles\[index\];
        let tempRatio = tempProfile.size.width >= tempProfile.size.height ?
          tempProfile.size.width / tempProfile.size.height : tempProfile.size.height / tempProfile.size.width;
        let currentRatio = Math.abs(tempRatio - surfaceRatio);
        if (currentRatio <= minRatioDiff && tempProfile.format == camera.CameraFormat.CAMERA\_FORMAT\_YUV\_420\_SP) {
          previewProfile = tempProfile;
          break;
        }
      }
      this.imageWidth = previewProfile.size.width; // 更新xComponent组件的宽。
      this.imageHeight = previewProfile.size.height; // 更新xComponent组件的高。
      console.info(\`initCamera imageWidth:${this.imageWidth} imageHeight:${this.imageHeight}\`);

      // 使用xComponentSurfaceId创建预览。
      this.previewOutput = this.cameraManager.createPreviewOutput(previewProfile, this.xComponentSurfaceId);
      if (!this.previewOutput) {
        console.error('initCamera createPreviewOutput');
        this.releaseCamera();
        return;
      }
      // 创建录像模式相机会话。
      let session = this.cameraManager.createSession(camera.SceneMode.NORMAL\_VIDEO);
      if (!session) {
        console.error('session is null');
        this.releaseCamera();
        return;
      }
      this.session = session as camera.VideoSession;
      // 开始配置会话。
      this.session.beginConfig();
      // 添加相机设备输入。
      this.session.addInput(this.cameraInput);
      // 添加预览流输出。
      this.session.addOutput(this.previewOutput);
      // 提交会话配置。
      await this.session.commitConfig();
      // 开始启动已配置的输入输出流。
      await this.session.start();
    } catch (error) {
      console.error(\`initCamera fail: ${JSON.stringify(error)}\`);
      this.releaseCamera();
    }
  }

  // 释放相机。
  async releaseCamera(): Promise<void> {
    console.info('releaseCamera');
    // 停止当前会话。
    await this.session?.stop().catch((e: BusinessError) => {console.error('Failed to stop session: ', e)});
    // 释放相机输入流。
    await this.cameraInput?.close().catch((e: BusinessError) => {console.error('Failed to close the camera: ', e)});
    // 释放预览输出流。
    await this.previewOutput?.release().catch((e: BusinessError) => {console.error('Failed to stop the preview stream: ', e)});
    // 释放会话。
    await this.session?.release().catch((e: BusinessError) => {console.error('Failed to release session: ', e)});
  }
}