跳到主要内容

双路预览(ArkTS)

在开发相机应用时,需要先申请相关权限

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

相机应用通过控制相机,实现图像显示(预览)、照片保存(拍照)、视频录制(录像)等基础操作。相机开发模型为Surface模型,即应用通过Surface进行数据传递,通过ImageReceiver的Surface获取拍照流的数据、通过XComponent的Surface获取预览流的数据。

如果要实现双路预览,可以先参考拍照,在双路预览中将拍照流改为另一路预览流,通过ImageReceiver的Surface创建另一个previewOutput,其余流程与拍照一致。

详细的API说明请参考@ohos.multimedia.camera (相机管理)

约束与限制

  • 暂不支持动态添加流,即不能在没有调用session.stop的情况下,调用addOutput添加流。
  • 对ImageReceiver组件获取到的图像数据处理后,需要将对应的图像Buffer释放,确保Surface的BufferQueue正常轮转。

调用流程

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

开发步骤

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

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

用于处理图像的第一路预览流

  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中的format参数,PixelMap格式请参考PixelMapFormat

    // 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

    • 在通过createPixelMap接口创建PixelMap实例时,设置的Size、srcPixelFormat等属性必须和相机预览输出流previewProfile中配置的Size、Format属性保持一致,ImageReceiver图片像素格式请参考PixelMapFormat,相机预览输出流previewProfile输出格式请参考CameraFormat
    • 由于不同设备产品差异性,应用开发者在创建相机预览输出流前,必须先通过getSupportedOutputCapability方法获取当前设备支持的预览输出流previewProfile,再根据实际业务需求选择CameraFormatSize适合的预览输出流previewProfile。
    • ImageReceiver接收预览流图像数据实际format格式由应用开发者在创建预览输出流相机预览输出流时,根据实际业务需求选择的previewProfile中format格式参数影响,详细步骤请参考创建预览流获取数据
    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 解析图片buffer数据参考:

    需要确认图像的宽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的接口处理。

用于显示画面的第二路预览流

获取第二路预览流SurfaceId:创建XComponent组件用于预览流显示,获取SurfaceId请参考XComponent组件提供的getXComponentSurfaceId方法,而XComponent的能力由UI提供,相关介绍可参考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 });
// ...
})

创建预览流获取数据

通过两个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');
});
});
});
}
}