HarmonyOS的相机API分散在@kit.MultimediaKit里,photoAccessHelper管相册、camera管拍照、media管录像。三条线各自独立,合在一起才是完整相机功能。这篇把拍照和录像的主线走通,顺带说清楚权限和生命周期。

权限声明
相机开发需要三个权限:
json5
// module.json5
{
"requestPermissions": [
{ "name": "ohos.permission.CAMERA" },
{ "name": "ohos.permission.MICROPHONE" },
{ "name": "ohos.permission.READ_IMAGEVIDEO" }
]
}
CAMERA和MICROPHONE是用户授权权限,运行时必须动态申请。READ_IMAGEVIDEO用于保存照片到相册。三个权限缺一个功能就残废。
动态申请:
typescript
import { abilityAccessCtrl, common } from '@kit.AbilityKit';
async requestPermissions(context: common.UIAbilityContext): Promise<boolean> {
let atManager: abilityAccessCtrl.AccessManager = abilityAccessCtrl.createAtManager();
let permissions: string[] = ['ohos.permission.CAMERA', 'ohos.permission.MICROPHONE'];
let result = await atManager.requestPermissionsFromUser(context, permissions);
for (let i = 0; i < result.authResults.length; i++) {
if (result.authResults[i] !== 0) {
return false;
}
}
return true;
}
authResults为0表示授权通过。不通过的要引导用户去设置页手动开启。
CameraManager初始化
typescript
import { camera } from '@kit.MultimediaKit';
let cameraManager: camera.CameraManager = camera.getCameraManager(context);
就一行,但context必须是UIAbilityContext,不是BaseContext。在UIAbility里用this.context,在页面里用getContext(this)。
获取可用相机列表:
typescript
let cameras: camera.CameraDevice[] = cameraManager.getSupportedCameras();
// cameras[0]通常是后置, cameras[1]通常是前置
getSupportedCameras返回的数组长度取决于设备。模拟器可能返回空数组------相机API基本都要真机测试。

创建会话
拍照用PhotoSession,录像用VideoSession:
typescript
import { camera } from '@kit.MultimediaKit';
// 拍照会话
let photoSession: camera.PhotoSession = cameraManager.createPhotoSession(cameras[0]);
// 录像会话
let videoSession: camera.VideoSession = cameraManager.createVideoSession(cameras[0]);
Session是相机操作的核心。所有配置和操作都挂在Session上。
预览
预览需要XComponent作为输出面:
typescript
XComponent({ id: 'cameraPreview', type: 'surface', controller: this.xComponentController })
.onLoad(() => {
let surfaceId: string = this.xComponentController.getXComponentSurfaceId();
// 将surfaceId设置到session的预览输出
let previewOutput: camera.PreviewOutput = cameraManager.createPreviewOutput(profile, surfaceId);
photoSession.addOutput(previewOutput);
photoSession.start();
})
流程:XComponent提供surface → 创建PreviewOutput → 添加到Session → start。
Profile从cameraManager.createProfiles获取:
typescript
let profiles: camera.CameraOutputCapability = cameraManager.getSupportedOutputCapability(cameras[0]);
let previewProfile: camera.Profile = profiles.previewProfiles[0];
let photoProfile: camera.PhotoProfile = profiles.photoProfiles[0];
取第一个Profile是最简单的做法,生产环境要根据分辨率和帧率筛选。
拍照
typescript
let photoOutput: camera.PhotoOutput = cameraManager.createPhotoOutput(photoProfile, surfaceId);
photoSession.addOutput(photoOutput);
// 设置拍照回调
photoOutput.on('photoAvailable', (photo: camera.Photo): void => {
// photo可以保存到相册
});
// 触发拍照
photoSession.capture();
capture()是异步的,拍照结果通过photoAvailable回调返回。回调里拿到的是Photo对象,需要进一步处理才能保存。
保存到相册
typescript
import { photoAccessHelper } from '@kit.MediaLibraryKit';
async savePhoto(photo: camera.Photo, context: common.UIAbilityContext): Promise<string> {
let helper: photoAccessHelper.PhotoAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
let uri: string = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');
let file: fileIo.File = fileIo.openSync(uri, fileIo.OpenMode.WRITE_ONLY);
// 从photo中读取数据并写入file
// ...
fileIo.closeSync(file);
return uri;
}
createAsset创建相册条目,返回uri。然后把照片数据写入uri对应的文件。
录像
录像需要AVRecorder配合VideoSession:
typescript
import { media } from '@kit.MultimediaKit';
// 创建VideoOutput
let videoOutput: camera.VideoOutput = cameraManager.createVideoOutput(videoProfile, surfaceId);
videoSession.addOutput(videoOutput);
// 创建AVRecorder
let avRecorder: media.AVRecorder = await media.createAVRecorder();
let config: media.AVRecorderConfig = {
videoSourceType: media.VideoSourceType.CAMERA_SOURCE,
profile: {
fileFormat: media.ContainerFormatType.CAMERA_MP4,
videoBitrate: 2000000,
videoCodec: media.VideoCodecType.H264,
videoFrameWidth: 1920,
videoFrameHeight: 1080,
videoFrameRate: 30
},
url: 'fd://' + fileDescriptor
};
await avRecorder.prepare(config);
// 关联videoOutput和avRecorder
videoOutput.start();
await avRecorder.start();
录像流程比较长:createAVRecorder → prepare → videoOutput.start → avRecorder.start。每一步都必须等上一步完成。
停止录像:
typescript
await avRecorder.stop();
await avRecorder.release();
videoOutput.stop();
切换前后摄像头
typescript
async switchCamera(session: camera.PhotoSession, cameraManager: camera.CameraManager,
newCamera: camera.CameraDevice): Promise<void> {
await session.stop();
session.removeOutput(previewOutput);
session.removeOutput(photoOutput);
// 重新创建session和output
let newSession: camera.PhotoSession = cameraManager.createPhotoSession(newCamera);
// 重新addOutput和start
}
切换摄像头不能直接替换camera,必须停止session、移除旧output、创建新session。简单粗暴但有效。
闪光灯
typescript
// 检查是否支持闪光灯
let hasFlash: boolean = photoSession.hasFlash();
// 设置闪光灯模式
photoSession.setFlashMode(camera.FlashMode.FLASH_MODE_ALWAYS_ON); // 常开
photoSession.setFlashMode(camera.FlashMode.FLASH_MODE_AUTO); // 自动
photoSession.setFlashMode(camera.FlashMode.FLASH_MODE_CLOSED); // 关闭
// 获取当前模式
let mode: camera.FlashMode = photoSession.getFlashMode();
前置摄像头通常不支持闪光灯,hasFlash()返回false。这种情况下调setFlashMode会报错。
变焦
typescript
// 获取变焦范围
let zoomRange: Array<number> = photoSession.getZoomRatioRange();
let minZoom: number = zoomRange[0]; // 如1.0
let maxZoom: number = zoomRange[1]; // 如6.0
// 设置变焦倍数
photoSession.setZoomRatio(2.0);
// 获取当前变焦
let currentZoom: number = photoSession.getZoomRatio();
setZoomRatio的值必须在zoomRatioRange范围内。超出范围直接报参数错误。
对焦
typescript
// 设置对焦模式
photoSession.setFocusMode(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO); // 连续自动对焦
photoSession.setFocusMode(camera.FocusMode.FOCUS_MODE_MANUAL); // 手动对焦
// 手动对焦指定点
photoSession.setFocusPoint({ x: 0.5, y: 0.5 }); // 归一化坐标(0-1)
手动对焦需要先设FOCUS_MODE_MANUAL,再设焦点坐标。坐标是归一化的------(0,0)左上角,(1,1)右下角。
分辨率选择
typescript
let outputCapability: camera.CameraOutputCapability = cameraManager.getSupportedOutputCapability(cameraDevice);
// 遍历可用的photoProfile
let photoProfiles: camera.PhotoProfile[] = outputCapability.photoProfiles;
for (let i = 0; i < photoProfiles.length; i++) {
let profile: camera.PhotoProfile = photoProfiles[i];
let width: number = profile.size.width;
let height: number = profile.size.height;
// 选择需要的分辨率
}
photoProfiles按分辨率从高到低排列。取0通常是最高分辨率。
状态监听
typescript
// Session状态
photoSession.on('stateChange', (state: camera.SessionState) => {
switch (state) {
case camera.SessionState.SESSION_CONFIGURED:
// 配置完成
break;
case camera.SessionState.SESSION_STARTED:
// 已启动
break;
case camera.SessionState.SESSION_STOPPED:
// 已停止
break;
}
});
// 错误监听
photoSession.on('error', (error: camera.BusinessError) => {
// 处理相机错误
});
SessionState有CONFIGURING、CONFIGURED、STARTED、STOPPED四种状态。createPhotoSession后进入CONFIGURING,addOutput后到CONFIGURED,start后到STARTED。

生命周期管理
相机资源必须正确释放,否则其他应用无法使用相机:
typescript
aboutToDisappear(): void {
if (this.photoSession) {
this.photoSession.stop();
this.photoSession.release();
}
if (this.cameraManager) {
this.cameraManager.release();
}
}
release顺序:Session → Output → CameraManager。每个release都是异步的,应该await确保完成。
踩坑清单
| 问题 | 原因 | 解决 |
|---|---|---|
| 模拟器getSupportedCameras返回空 | 模拟器没有摄像头 | 用真机测试 |
| getCameraManager报错 | context类型不对 | 传UIAbilityContext |
| 预览黑屏 | surfaceId没传或session没start | 检查XComponent onLoad流程 |
| capture()无回调 | 没addOutput(photoOutput) | 必须把photoOutput加入session |
| 切换摄像头crash | 直接替换camera | stop→removeOutput→新建session |
| 闪光灯报错 | 前置不支持 | 先hasFlash()检查 |
| 变焦报错 | 超出zoomRatioRange | 在范围内设值 |
| 录像文件打不开 | AVRecorder没prepare直接start | 按prepare→start顺序 |
| 相机被占用 | 上次没release | aboutToDisappear中释放 |
| 保存相册失败 | 缺READ_IMAGEVIDEO权限 | module.json5声明权限 |
相机开发最大的痛点是调试------模拟器几乎不能跑,真机每次部署又慢。建议先把权限和初始化流程写对,再逐步加功能。拍照→保存→预览→录像,一个功能调通再加下一个。