跳到主要内容HarmonyOS 相机开发实战指南 | 极客日志TypeScript大前端
HarmonyOS 相机开发实战指南
HarmonyOS Camera Kit 相机开发流程,涵盖权限申请、架构解析、核心 API 使用(CameraManager、Session、Output)、多模式切换及资源释放。通过实战案例与避坑指南,帮助开发者快速掌握预览、拍照、录像功能,避免常见配置错误与权限问题。
不知所云14K 浏览 背景引入
本文介绍 HarmonyOS Camera Kit(相机服务)的开发流程。Camera Kit 是系统提供的相机开发套件,用于精确控制硬件资源。
为什么使用 Camera Kit?
在应用内实现拍照功能时,开发者通常面临以下选择:
- 自行编写底层驱动:涉及 ISP、HDI、缓存队列等硬件细节,开发成本高。
- 调用系统相机:简单但定制性差。
- 使用 Camera Kit:系统提供的开发套件,支持预览、拍照、录像及硬件参数控制。
优势:
- 无需编写底层驱动
- 支持闪光灯、曝光时间、对焦等精细控制
- 适配多镜头(广角、长焦、TOF 等)
核心功能概览
| 功能 | 说明 | 难度 |
|---|
| 预览 | 实时显示相机画面 | ⭐ 简单 |
| 拍照 | 拍摄并保存照片 | ⭐⭐ 中等 |
| 录像 | 录制视频(含音频) | ⭐⭐⭐ 较复杂 |
| 闪光灯控制 | 开关、自动、常亮 | ⭐ 简单 |
| 对焦调焦 | 自动对焦、手动对焦 | ⭐⭐ 中等 |
| 曝光控制 | 调整曝光时间、ISO | ⭐⭐⭐ 较复杂 |
| 多摄同开 | 同时开启多个摄像头 | ⭐⭐⭐⭐ 困难 |
注意:多摄同开功能受设备硬件限制,使用前需检查兼容性。
整体架构
Camera Kit 架构主要包含三层:
┌─────────────────────────────────────────┐
│ 你的应用 │
│ (创建会话、配置输入输出) │
└─────────────────┬───────────────────────┘
▼
┌─────────────────────────────────────────┐
│ Camera Kit 服务 │
│ (会话管理、设备管理、输出管理) │
└─────────────────┬───────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 相机硬件 + ISP │
│ (真正的采集和处理) │
└─────────────────────────────────────────┘
工作流程:
- 获取
CameraManager(相机管理器)
- 创建
CameraInput(相机输入)—— 选择摄像头
- 创建
CameraSession(相机会话)—— 配置拍摄模式
- 添加
Output(输出流)—— 预览流、拍照流、录像流
- 提交配置,启动会话
- 执行拍照/录像操作
- 释放资源
关键概念:
- CameraManager:管理所有相机设备的入口。
- CameraDevice:单个物理相机设备(如前置、后置)。
CameraInput:相机输入流,指定拍摄源。CameraSession:相机会话,封装拍摄模式和输出配置。Output:输出流,负责数据流转(预览、拍照、录像)。开发准备:权限申请
3.1 声明权限
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:camera_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:mic_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
CAMERA:相机权限,必需。
MICROPHONE:麦克风权限,录制视频时需要。
MEDIA_LOCATION:如需在照片元数据中记录地理位置,需额外添加。
3.2 运行时授权
import { abilityAccessCtrl, bundleManager } from '@kit.AbilityKit';
async function requestCameraPermission(): Promise<boolean> {
let atManager = abilityAccessCtrl.createAtManager();
let token = await bundleManager.getAccessToken();
let grantStatus = await atManager.checkAccessToken(token, 'ohos.permission.CAMERA');
if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
console.info('已有相机权限');
return true;
}
let result = await atManager.requestPermissionsFromUser([
'ohos.permission.CAMERA',
'ohos.permission.MICROPHONE'
]);
for (let permResult of result.authResults) {
if (permResult !== 0) {
console.error('用户拒绝了权限');
return false;
}
}
console.info('权限申请成功');
return true;
}
建议:在应用启动阶段申请权限,避免用户点击拍照时才弹窗影响体验。
核心功能实现
4.1 获取相机管理器
import { camera } from '@kit.CameraKit';
import { common } from '@kit.AbilityKit';
function getCameraManager(context: common.BaseContext): camera.CameraManager | undefined {
let cameraManager: camera.CameraManager;
try {
cameraManager = camera.getCameraManager(context);
} catch (error) {
console.error(`获取相机管理器失败:${error}`);
return undefined;
}
return cameraManager;
}
注意:若获取失败,可能因相机被占用或设备无相机硬件,应终止后续操作。
4.2 获取支持的相机列表
function getSupportedCameras(
cameraManager: camera.CameraManager
): Array<camera.CameraDevice> {
let cameraArray: Array<camera.CameraDevice> = cameraManager.getSupportedCameras();
if (cameraArray != undefined && cameraArray.length > 0) {
for (let index = 0; index < cameraArray.length; index++) {
console.info(`相机 ID: ${cameraArray[index].cameraId}`);
console.info(`相机位置:${cameraArray[index].cameraPosition}`);
console.info(`相机类型:${cameraArray[index].cameraType}`);
console.info(`连接类型:${cameraArray[index].connectionType}`);
}
return cameraArray;
} else {
console.error('设备没有可用相机');
return [];
}
}
4.3 监听相机状态
function onCameraStatusChange(cameraManager: camera.CameraManager): void {
cameraManager.on('cameraStatus', (err, cameraStatusInfo) => {
if (err != undefined && err.code !== 0) {
console.error(`相机状态监听错误:${err.code}`);
return;
}
if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_APPEAR) {
console.info('新相机设备出现');
}
if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_DISAPPEAR) {
console.info('相机设备被移除');
}
if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_AVAILABLE) {
console.info('相机当前可用');
}
if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_UNAVAILABLE) {
console.info('相机被占用');
}
console.info(`相机 ID: ${cameraStatusInfo.camera.cameraId}`);
console.info(`状态:${cameraStatusInfo.status}`);
});
}
4.4 创建相机输入流
async function createInput(
cameraDevice: camera.CameraDevice,
cameraManager: camera.CameraManager
): Promise<camera.CameraInput | undefined> {
let cameraInput: camera.CameraInput | undefined = undefined;
try {
cameraInput = cameraManager.createCameraInput(cameraDevice);
} catch (error) {
let err = error as BusinessError;
console.error(`创建相机输入失败:${err.code}`);
}
if (cameraInput === undefined) {
return undefined;
}
cameraInput.on('error', cameraDevice, (error: BusinessError) => {
console.error(`相机输入错误:${error.code}`);
});
await cameraInput.open();
return cameraInput;
}
4.5 获取支持的模式
function getSupportedSceneModes(
cameraDevice: camera.CameraDevice,
cameraManager: camera.CameraManager
): Array<camera.SceneMode> {
let sceneModeArray: Array<camera.SceneMode> = cameraManager.getSupportedSceneModes(cameraDevice);
if (sceneModeArray != undefined && sceneModeArray.length > 0) {
for (let index = 0; index < sceneModeArray.length; index++) {
console.info(`支持的模式:${sceneModeArray[index]}`);
}
return sceneModeArray;
} else {
console.error('获取支持的模式失败');
return [];
}
}
NORMAL_PHOTO:普通拍照
NORMAL_VIDEO:普通录像
HIGH_QUALITY_PHOTO:高质量拍照
4.6 获取输出能力
async function getSupportedOutputCapability(
cameraDevice: camera.CameraDevice,
cameraManager: camera.CameraManager,
sceneMode: camera.SceneMode
): Promise<camera.CameraOutputCapability | undefined> {
let cameraOutputCapability: camera.CameraOutputCapability = cameraManager.getSupportedOutputCapability(
cameraDevice,
sceneMode
);
if (!cameraOutputCapability) {
console.error('获取输出能力失败');
return undefined;
}
console.info(`输出能力:${JSON.stringify(cameraOutputCapability)}`);
let previewProfilesArray: Array<camera.Profile> = cameraOutputCapability.previewProfiles;
let photoProfilesArray: Array<camera.Profile> = cameraOutputCapability.photoProfiles;
if (!previewProfilesArray) {
console.error('不支持预览流');
}
if (!photoProfilesArray) {
console.error('不支持拍照流');
}
return cameraOutputCapability;
}
会话管理
5.1 创建会话
function getSession(cameraManager: camera.CameraManager): camera.VideoSession | undefined {
let videoSession: camera.VideoSession | undefined = undefined;
try {
videoSession = cameraManager.createSession(camera.SceneMode.NORMAL_VIDEO) as camera.VideoSession;
} catch (error) {
let err = error as BusinessError;
console.error(`创建会话失败:${err.code}`);
}
return videoSession;
}
注意:会话类型需与场景模式匹配(录像用 VideoSession,拍照用 PhotoSession)。
5.2 配置会话
function beginConfig(videoSession: camera.VideoSession): void {
try {
videoSession.beginConfig();
} catch (error) {
let err = error as BusinessError;
console.error(`开始配置失败:${err.code}`);
}
}
5.3 添加输入输出流
async function startSession(
videoSession: camera.VideoSession,
cameraInput: camera.CameraInput,
previewOutput: camera.PreviewOutput,
photoOutput: camera.PhotoOutput
): Promise<void> {
try {
videoSession.addInput(cameraInput);
} catch (error) {
let err = error as BusinessError;
console.error(`添加输入流失败:${err.code}`);
}
let canAddPreviewOutput: boolean = false;
try {
canAddPreviewOutput = videoSession.canAddOutput(previewOutput);
} catch (error) {
let err = error as BusinessError;
console.error(`检查预览流失败:${err.code}`);
}
if (!canAddPreviewOutput) {
console.error('无法添加预览输出流');
return;
}
try {
videoSession.addOutput(previewOutput);
} catch (error) {
let err = error as BusinessError;
console.error(`添加预览流失败:${err.code}`);
}
let canAddPhotoOutput: boolean = false;
try {
canAddPhotoOutput = videoSession.canAddOutput(photoOutput);
} catch (error) {
let err = error as BusinessError;
console.error(`检查拍照流失败:${err.code}`);
}
if (!canAddPhotoOutput) {
console.error('无法添加拍照输出流');
return;
}
try {
videoSession.addOutput(photoOutput);
} catch (error) {
let err = error as BusinessError;
console.error(`添加拍照流失败:${err.code}`);
}
try {
await videoSession.commitConfig();
} catch (error) {
let err = error as BusinessError;
console.error(`提交配置失败:${err.code}`);
return;
}
try {
await videoSession.start();
} catch (error) {
let err = error as BusinessError;
console.error(`启动会话失败:${err.code}`);
}
}
- 添加输出流前先用
canAddOutput 检查。
- 必须先提交配置 (
commitConfig) 才能启动会话。
- 顺序不可颠倒。
5.4 会话切换
async function switchOutput(
videoSession: camera.VideoSession,
videoOutput: camera.VideoOutput,
photoOutput: camera.PhotoOutput
): Promise<void> {
try {
await videoSession.stop();
} catch (error) {
let err = error as BusinessError;
console.error(`停止会话失败:${err.code}`);
}
try {
videoSession.beginConfig();
} catch (error) {
let err = error as BusinessError;
console.error(`开始配置失败:${err.code}`);
}
try {
videoSession.removeOutput(photoOutput);
} catch (error) {
let err = error as BusinessError;
console.error(`移除拍照流失败:${err.code}`);
}
try {
videoSession.canAddOutput(videoOutput);
} catch (error) {
let err = error as BusinessError;
console.error(`检查视频流失败:${err.code}`);
}
try {
videoSession.addOutput(videoOutput);
} catch (error) {
let err = error as BusinessError;
console.error(`添加视频流失败:${err.code}`);
}
try {
await videoSession.commitConfig();
} catch (error) {
let err = error as BusinessError;
console.error(`提交配置失败:${err.code}`);
}
try {
await videoSession.start();
} catch (error) {
let err = error as BusinessError;
console.error(`启动会话失败:${err.code}`);
}
}
建议:切换模式时给用户 Loading 提示,避免误以为卡死。
常见问题与解决方案
问题 1:权限未申请直接调用
let cameraManager = camera.getCameraManager(context);
let hasPermission = await requestCameraPermission();
if (!hasPermission) {
promptAction.showToast({ message: '需要相机权限' });
return;
}
let cameraManager = camera.getCameraManager(context);
问题 2:相机被占用未处理
let cameraInput = cameraManager.createCameraInput(cameraDevice);
cameraManager.on('cameraStatus', (err, info) => {
if (info.status === camera.CameraStatus.CAMERA_STATUS_AVAILABLE) {
let cameraInput = cameraManager.createCameraInput(cameraDevice);
}
});
问题 3:输出流添加顺序错误
session.addOutput(previewOutput);
session.addInput(cameraInput);
session.addInput(cameraInput);
session.addOutput(previewOutput);
session.addOutput(photoOutput);
问题 4:未检查 canAddOutput
session.addOutput(previewOutput);
let canAdd = session.canAddOutput(previewOutput);
if (!canAdd) {
console.error('设备不支持此输出流');
return;
}
session.addOutput(previewOutput);
问题 5:会话配置完未提交
session.addInput(cameraInput);
session.addOutput(previewOutput);
session.start();
session.addInput(cameraInput);
session.addOutput(previewOutput);
await session.commitConfig();
await session.start();
问题 6:释放资源不及时
function closeCamera() {
return;
}
async function closeCamera(session: camera.Session, cameraInput: camera.CameraInput) {
await session.stop();
await cameraInput.close();
session.release();
}
问题 7:模拟器测试相机
let cameras = cameraManager.getSupportedCameras();
完整实战案例
场景:简单的拍照应用
import { camera } from '@kit.CameraKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
export class SimpleCameraManager {
private cameraManager: camera.CameraManager | undefined;
private cameraDevice: camera.CameraDevice | undefined;
private cameraInput: camera.CameraInput | undefined;
private session: camera.PhotoSession | undefined;
private previewOutput: camera.PreviewOutput | undefined;
private photoOutput: camera.PhotoOutput | undefined;
async init(context: common.BaseContext): Promise<boolean> {
this.cameraManager = camera.getCameraManager(context);
if (!this.cameraManager) {
console.error('获取相机管理器失败');
return false;
}
let cameras = this.cameraManager.getSupportedCameras();
if (cameras.length === 0) {
console.error('没有可用相机');
return false;
}
this.cameraDevice =
cameras.find(c => c.cameraPosition === camera.CameraPosition.BACK) || cameras[0];
this.cameraInput = this.cameraManager.createCameraInput(this.cameraDevice);
await this.cameraInput.open();
return true;
}
async createSession(): Promise<boolean> {
if (!this.cameraManager || !this.cameraDevice) {
return false;
}
this.session = this.cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
this.session.beginConfig();
this.session.addInput(this.cameraInput);
await this.session.commitConfig();
await this.session.start();
return true;
}
async takePhoto(): Promise<string | undefined> {
if (!this.photoOutput) {
console.error('拍照输出流未创建');
return undefined;
}
let photoPath = await this.photoOutput.capture();
console.info(`照片保存路径:${photoPath}`);
return photoPath;
}
async release(): Promise<void> {
if (this.session) {
await this.session.stop();
this.session.release();
}
if (this.cameraInput) {
await this.cameraInput.close();
}
}
}
let cameraManager = new SimpleCameraManager();
await cameraManager.init(context);
await cameraManager.createSession();
let photoPath = await cameraManager.takePhoto();
await cameraManager.release();
总结
Camera Kit 能力
- 预览、拍照、录像
- 闪光灯、对焦、曝光控制
- 多摄同开(部分设备支持)
开发流程
- 申请权限(CAMERA、MICROPHONE)
- 获取 CameraManager
- 获取相机列表,选择一个
- 创建 CameraInput
- 创建 CameraSession
- 添加输入输出流
- 提交配置,启动会话
- 拍照/录像
- 释放资源
常见坑点
- 权限未申请直接调用
- 相机被占用未处理
- 输出流添加顺序错误
- 未检查 canAddOutput
- 会话配置完未提交
- 释放资源不及时
- 在模拟器上测试
建议
- 简单拍照功能可使用
CameraPicker,无需自行实现 Camera Kit。
- 深度定制需关注:相机状态监听、canAddOutput 检查、资源及时释放。
- 务必使用真机测试,模拟器无法模拟相机硬件。
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online