双路预览(ArkTS)
===========

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

双路预览，即应用可同时使用两路预览流，一路用于在屏幕上显示，一路用于图像处理等其他操作，提升处理效率。

相机应用通过控制相机，实现图像显示（预览）、照片保存（拍照）、视频录制（录像）等基础操作。相机开发模型为Surface模型，即应用通过Surface进行数据传递，通过[ImageReceiver](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-image-imagereceiver)的Surface获取拍照流的数据、通过XComponent的Surface获取预览流的数据。

如果要实现双路预览，可以先参考[拍照](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/camera-shooting)，在双路预览中将拍照流改为另一路预览流，通过[ImageReceiver](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-image-imagereceiver)的Surface创建另一个previewOutput，其余流程与拍照一致。

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

#### 约束与限制

*   暂不支持动态添加流，即不能在没有调用[session.stop](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-session#stop11)的情况下，调用[addOutput](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-session#addoutput11)添加流。
*   对ImageReceiver组件获取到的图像数据处理后，需要将对应的图像Buffer释放，确保Surface的BufferQueue正常轮转。

#### 调用流程

双路方案调用流程图建议如下：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/b1/v3/HLFpF6WuQeqjdHnzRDiO7w/zh-cn_image_0000002594668824.png?HW-CC-KV=V1&HW-CC-Date=20260612T073512Z&HW-CC-Expire=86400&HW-CC-Sign=616A056F8A74850A0154E4C04BF38A891E329B23BFD89EB5C2ACD03A41A407ED)

#### 开发步骤

*   用于处理图像的第一路预览流：创建ImageReceiver对象，获取SurfaceId创建第一路预览流，注册图像监听，按需处理预览流每帧图像。
*   用于显示画面的第二路预览流：创建XComponent组件，获取SurfaceId创建第二路预览流，预览流画面直接在组件内渲染。
*   创建预览流获取数据：创建上述两路预览流，配置进相机会话，启动会话后，两路预览流同时获取数据。

以下各步骤示例为片段代码，可通过点击示例代码右下方的链接获取完整工程示例。

#### \[h2\]用于处理图像的第一路预览流

1.  导入依赖，本篇文档需要用到图片和相机框架等相关依赖包。
    
    import { image } from '@kit.ImageKit';
    import { camera } from '@kit.CameraKit';
    import { display } from '@kit.ArkUI';
    import { BusinessError } from '@kit.BasicServicesKit';
    
2.  获取第一路预览流SurfaceId：创建ImageReceiver对象，通过ImageReceiver对象可获取其SurfaceId。
    
    async init(size: Size, format = image.ImageFormat.JPEG, capacity = 8) {
      const receiver = image.createImageReceiver(size, format, capacity);
      const surfaceId = await receiver.getReceivingSurfaceId();
      this.onImageArrival(receiver);
      return surfaceId;
    }
    
3.  ImageReceiver接收预览流图像数据获取图像格式请参考[Image](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-image-image)中的format参数，[PixelMap](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-image-pixelmap)格式请参考[PixelMapFormat](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-image-e#pixelmapformat7)。
    
    // Image格式与PixelMap格式映射关系。
    let formatToPixelMapFormatMap = new Map<number, image.PixelMapFormat>(\[
      \[12, image.PixelMapFormat.RGBA\_8888\],
      \[25, image.PixelMapFormat.NV21\],
      \[35, image.PixelMapFormat.YCBCR\_P010\],
      \[36, image.PixelMapFormat.YCRCB\_P010\]
    \]);
    // PixelMapFormat格式的单个像素点大小映射关系。
    let pixelMapFormatToSizeMap = new Map<image.PixelMapFormat, number>(\[
      \[image.PixelMapFormat.RGBA\_8888, 4\],
      \[image.PixelMapFormat.NV21, 1.5\],
      \[image.PixelMapFormat.YCBCR\_P010, 3\],
      \[image.PixelMapFormat.YCRCB\_P010, 3\]
    \]);
    
4.  注册监听处理预览流每帧图像数据：通过ImageReceiver组件中imageArrival事件监听获取底层返回的图像数据，详细的API说明请参考[ImageReceiver](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-image-imagereceiver)。
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/aa/v3/Ejz_9csdS9O0lNwLBWe1Ag/note_3.0-zh-cn.png?HW-CC-KV=V1&HW-CC-Date=20260612T073512Z&HW-CC-Expire=86400&HW-CC-Sign=DE7BD87B23E6F210014C94C1D1A06E2C38529A70CC51E7DAFD43EBC446997A88)
    
    *   在通过[createPixelMap](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-image-f#imagecreatepixelmap8)接口创建[PixelMap](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-image-pixelmap)实例时，设置的Size、srcPixelFormat等属性必须和相机预览输出流previewProfile中配置的Size、Format属性保持一致，ImageReceiver图片像素格式请参考[PixelMapFormat](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-image-e#pixelmapformat7)，相机预览输出流previewProfile输出格式请参考[CameraFormat](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-e#cameraformat)。
    *   由于不同设备产品差异性，应用开发者在创建相机预览输出流前，必须先通过[getSupportedOutputCapability](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-cameramanager#getsupportedoutputcapability11)方法获取当前设备支持的预览输出流previewProfile，再根据实际业务需求选择[CameraFormat](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-e#cameraformat)和[Size](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-camera-i#size)适合的预览输出流previewProfile。
    *   ImageReceiver接收预览流图像数据实际format格式由应用开发者在创建预览输出流相机预览输出流时，根据实际业务需求选择的previewProfile中format格式参数影响，详细步骤请参考[创建预览流获取数据](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/camera-dual-channel-preview#创建预览流获取数据)。
    
    onImageArrival(receiver: image.ImageReceiver): void {
      receiver.on('imageArrival', () => {
        Logger.info(TAG, 'image arrival');
        receiver.readNextImage((err: BusinessError, nextImage: image.Image) => {
          if (err || nextImage === undefined) {
            Logger.error(TAG, 'readNextImage failed');
            return;
          }
          nextImage.getComponent(image.ComponentType.JPEG, async (err: BusinessError, imgComponent: image.Component) => {
            if (err || imgComponent === undefined) {
              Logger.error(TAG, 'getComponent failed');
            }
            if (imgComponent.byteBuffer) {
              // ...
            } else {
              Logger.error(TAG, 'byteBuffer is null');
            }
            // ...
          });
        });
      });
    }
    
    通过 [image.Component](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-image-i#component9) 解析图片buffer数据参考：
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/bd/v3/4NdrVZOeQ465Sldmx6uexA/caution_3.0-zh-cn.png?HW-CC-KV=V1&HW-CC-Date=20260612T073512Z&HW-CC-Expire=86400&HW-CC-Sign=8366F1611F20AC5B1238AB19DF85F55967DD470315B4B1AC5965B11679AA8ABC)
    
    需要确认图像的宽width是否与行距rowStride一致，如果不一致可参考以下方式处理：
    
    方式一：去除imgComponent.byteBuffer中stride数据，拷贝得到新的buffer，调用不支持stride的接口处理buffer。
    
    async getPixelMap(imgComponent: image.Component, width: number, height: number, stride: number) {
      if (stride === width) {
        return await image.createPixelMap(imgComponent.byteBuffer, {
          size: { height: height, width: width },
          srcPixelFormat: image.PixelMapFormat.NV21,
        });
      }
      const dstBufferSize = width \* height \* 1.5;
      const dstArr = new Uint8Array(dstBufferSize);
      for (let j = 0; j < height \* 1.5; j++) {
        const srcBuf = new Uint8Array(imgComponent.byteBuffer, j \* stride, width);
        dstArr.set(srcBuf, j \* width);
      }
      return await image.createPixelMap(dstArr.buffer, {
        size: { height: height, width: width },
        srcPixelFormat: image.PixelMapFormat.NV21,
      });
    }
    
    方式二：根据stride\*height创建pixelMap，然后调用pixelMap的cropSync方法裁剪掉多余的像素。
    
    // 创建pixelMap，width宽传行距stride的值。
    let pixelMap = await image.createPixelMap(imgComponent.byteBuffer, {
      size:{height: height, width: stride}, srcPixelFormat: pixelMapFormat});
    // 裁剪多余的像素。
    pixelMap.cropSync({size:{width:width, height:height}, x:0, y:0});
    
    方式三：将原始imgComponent.byteBuffer和stride信息一起传给支持stride的接口处理。
    

#### \[h2\]用于显示画面的第二路预览流

获取第二路预览流SurfaceId：创建XComponent组件用于预览流显示，获取SurfaceId请参考XComponent组件提供的[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)。

XComponent({
  type: XComponentType.SURFACE,
  controller: this.previewVM.xComponentController
})
  .size({ height: '100%', width: '100%' })
  .onLoad(async () => {
    // ...
    this.previewVM.surfaceId = this.previewVM.xComponentController.getXComponentSurfaceId();
    this.previewVM.setPreviewSize();
    this.previewVM.xComponentController.setXComponentSurfaceRotation({ lock: true });
    // ...
  })

#### \[h2\]创建预览流获取数据

通过两个SurfaceId分别创建两路预览流输出，加入相机会话，启动相机会话，获取预览流数据。

async createOutput(config: CreateOutputConfig) {
  const cameraOutputCap = config.cameraManager.getSupportedOutputCapability(config.device, config.sceneMode);
  const displayRatio = config.profile.size.width / config.profile.size.height;
  const profileWidth = config.profile.size.width;
  const previewProfile = cameraOutputCap.previewProfiles
    .sort((a, b) => Math.abs(a.size.width - profileWidth) - Math.abs(b.size.width - profileWidth))
    .find(pf => {
      const pfDisplayRatio = pf.size.width / pf.size.height;
      return pf.format === config.profile.format &&
        Math.abs(pfDisplayRatio - displayRatio) <= CameraConstant.PROFILE\_DIFFERENCE;
    });
  if (!previewProfile) {
    Logger.error(TAG\_LOG, 'Failed to get preview profile');
    return;
  }
  this.output = config.cameraManager.createPreviewOutput(previewProfile, config.surfaceId);
  this.addOutputListener(this.output);
  return this.output;
}

#### 完整示例

import { image } from '@kit.ImageKit';
import { camera } from '@kit.CameraKit';
import { display } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { Logger } from 'commons';
import OutputManager, { CreateOutputConfig } from './OutputManager';
import CameraConstant from '../constants/CameraConstants';

const TAG = 'ImageReceiverManager';

export class ImageReceiverManager implements OutputManager {
  public output?: camera.PreviewOutput;
  public isActive: boolean = true;
  public callback: (px: PixelMap) => void;
  private position: camera.CameraPosition = camera.CameraPosition.CAMERA\_POSITION\_BACK;

  constructor(cb: (px: PixelMap) => void) {
    this.callback = cb;
  }

  async createOutput(config: CreateOutputConfig) {
    const cameraOutputCap = config.cameraManager.getSupportedOutputCapability(config.device, config.sceneMode);
    const displayRatio = config.profile.size.width / config.profile.size.height;
    const profileWidth = config.profile.size.width;
    const previewProfile = cameraOutputCap.previewProfiles
      .sort((a, b) => Math.abs(a.size.width - profileWidth) - Math.abs(b.size.width - profileWidth))
      .find(pf => {
        const pfDisplayRatio = pf.size.width / pf.size.height;
        return pf.format === config.profile.format &&
          Math.abs(pfDisplayRatio - displayRatio) <= CameraConstant.PROFILE\_DIFFERENCE;
      });
    if (!previewProfile) {
      Logger.error(TAG, 'Failed to get preview profile');
      return;
    }
    const surfaceId = await this.init(config.profile.size);
    this.output = config.cameraManager.createPreviewOutput(previewProfile, surfaceId);
    this.position = config.device.cameraPosition;
    return this.output;
  }

  async release() {
    await this.output?.release();
    this.output = undefined;
  }

  async init(size: Size, format = image.ImageFormat.JPEG, capacity = 8) {
    const receiver = image.createImageReceiver(size, format, capacity);
    const surfaceId = await receiver.getReceivingSurfaceId();
    this.onImageArrival(receiver);
    return surfaceId;
  }

  async getPixelMap(imgComponent: image.Component, width: number, height: number, stride: number) {
    if (stride === width) {
      return await image.createPixelMap(imgComponent.byteBuffer, {
        size: { height: height, width: width },
        srcPixelFormat: image.PixelMapFormat.NV21,
      });
    }
    const dstBufferSize = width \* height \* 1.5;
    const dstArr = new Uint8Array(dstBufferSize);
    for (let j = 0; j < height \* 1.5; j++) {
      const srcBuf = new Uint8Array(imgComponent.byteBuffer, j \* stride, width);
      dstArr.set(srcBuf, j \* width);
    }
    return await image.createPixelMap(dstArr.buffer, {
      size: { height: height, width: width },
      srcPixelFormat: image.PixelMapFormat.NV21,
    });
  }

  onImageArrival(receiver: image.ImageReceiver): void {
    receiver.on('imageArrival', () => {
      Logger.info(TAG, 'image arrival');
      receiver.readNextImage((err: BusinessError, nextImage: image.Image) => {
        if (err || nextImage === undefined) {
          Logger.error(TAG, 'readNextImage failed');
          return;
        }
        nextImage.getComponent(image.ComponentType.JPEG, async (err: BusinessError, imgComponent: image.Component) => {
          if (err || imgComponent === undefined) {
            Logger.error(TAG, 'getComponent failed');
          }
          if (imgComponent.byteBuffer) {
            const width = nextImage.size.width;
            const height = nextImage.size.height;
            const stride = imgComponent.rowStride;
            Logger.info(TAG, \`getComponent with width:${width} height:${height} stride:${stride}\`);
            const pixelMap = await this.getPixelMap(imgComponent, width, height, stride);
            const displayRotation = display.getDefaultDisplaySync().rotation \* camera.ImageRotation.ROTATION\_90;
            const rotation = this.output!.getPreviewRotation(displayRotation);
            if (this.position === camera.CameraPosition.CAMERA\_POSITION\_FRONT) {
              if (displayRotation === 90 || displayRotation === 270) {
                await pixelMap.rotate((rotation + 180) % 360);
              } else {
                await pixelMap.rotate(rotation);
              }
              await pixelMap.flip(true, false);
            } else {
              await pixelMap.rotate(rotation);
            }
            this.callback(pixelMap);
          } else {
            Logger.error(TAG, 'byteBuffer is null');
          }
          nextImage.release().then(() => {Logger.info(TAG, 'image release done');}).catch((error: BusinessError) => {
            Logger.error(TAG, \`Release failed! Code ${error.code},message is ${error.message}\`);
          });
          Logger.info(TAG, 'image process done');
        });
      });
    });
  }
}