相机预览功能全面实现
1. 预览功能概述
相机预览是相机应用中最基本也是最重要的功能,它让用户能够实时看到相机捕捉的画面。在鸿蒙系统中,预览功能主要通过XComponent组件和Camera Kit的PreviewOutput来实现。
1.1 预览流程
- 创建XComponent组件,获取SurfaceId。
- 通过CameraManager创建PreviewOutput,传入SurfaceId。
- 创建相机会话(Session),将CameraInput和PreviewOutput添加到会话中。
- 配置并启动会话,开始预览。
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 双路预览的实现步骤
- 创建两个XComponent,获取两个SurfaceId。
- 创建两个PreviewOutput,分别绑定两个SurfaceId。
- 将两个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的配置、相机会话的管理、双路预览的实现以及状态监控和错误处理。通过以上步骤,开发者可以实现一个完整的相机预览功能。