别只会用系统相机了:用鸿蒙自己撸一个拍照 + 录像 App
大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~
本文目录:
一、CameraX API 思路:别怕,结构其实就三块
虽然大纲写的是 “CameraX API”,但在鸿蒙里你更多是这样几个角色:
- CameraManager:管理摄像头列表、创建 Session
- CameraInput:真正的“相机输入”,比如后置 / 前置
- PreviewOutput:预览流(给 UI 展示)
- PhotoOutput:拍照输出(JPEG 等)
- VideoOutput / Recorder:录像输出(MP4 等)
整体流程基本是:
- 获取
CameraManager - 选择一个
CameraInput(后置 / 前置) - 创建
PreviewOutput和PhotoOutput/VideoOutput - 将这些绑定到一个
CameraSession - 把 Preview 的 Surface 绑定到 ArkUI 的相机预览组件
- 调用
capture()/start()/stop()实现拍照与录像
1.1 初始化 CameraManager
import camera from '@ohos.multimedia.camera';
let cameraMgr: camera.CameraManager | null = null;
async function getCameraManager(): Promise<camera.CameraManager> {
if (cameraMgr) return cameraMgr;
return new Promise((resolve, reject) => {
camera.getCameraManager((err, mgr) => {
if (err || !mgr) {
console.error('获取 CameraManager 失败: ' + JSON.stringify(err));
reject(err);
return;
}
cameraMgr = mgr;
resolve(mgr);
});
});
}
1.2 选择摄像头(前/后置)
async function selectBackCamera(): Promise<camera.CameraDevice> {
const mgr = await getCameraManager();
const devices = mgr.getSupportedCameras();
// 简单选一个后置摄像头
const back = devices.find(d => d.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK);
if (!back) {
throw new Error('未找到后置摄像头');
}
return back;
}
1.3 创建相机输入 & 输出
interface CameraContext {
input: camera.CameraInput;
session: camera.CameraSession;
previewOutput: camera.PreviewOutput;
photoOutput: camera.PhotoOutput;
videoOutput?: camera.VideoOutput;
}
async function createCameraContext(surfaceIdForPreview: string): Promise<CameraContext> {
const mgr = await getCameraManager();
const device = await selectBackCamera();
// 1. 创建 CameraInput
const input = mgr.createCameraInput(device);
// 2. 预览输出(绑定 Surface ID)
const previewOutput = mgr.createPreviewOutput({
surfaceId: surfaceIdForPreview
});
// 3. 拍照输出
const photoOutput = mgr.createPhotoOutput();
// 4. 会话
const session = mgr.createSession();
// 5. 绑定
await session.beginConfig();
await session.addInput(input);
await session.addOutput(previewOutput);
await session.addOutput(photoOutput);
await session.commitConfig();
// 启动预览
await session.start();
return {
input,
session,
previewOutput,
photoOutput
};
}
⚠️ 提醒:不同 API 版本里方法名可能是
createCaptureSession/addInput/addOutput等,按你的 SDK 为准,但总体是“输入+输出+会话”的套路。
二、拍照 & 录像能力:按钮背后到底干了啥?
2.1 拍照流程
UI 点“拍照” → 调用
capture()→ 回调里拿到 JPEG buffer → 保存为文件 → 写入相册
async function takePhoto(ctx: CameraContext, savePath: string): Promise<void> {
return new Promise((resolve, reject) => {
ctx.photoOutput.capture({
quality: camera.QualityLevel.QUALITY_LEVEL_HIGH // 示例
}, async (err, photoData) => {
if (err) {
console.error('拍照失败:' + JSON.stringify(err));
reject(err);
return;
}
// photoData 里通常是图像二进制数据
// 这里你可以写入文件系统
try {
await saveBytesToFile(savePath, photoData.buffer);
resolve();
} catch (e) {
reject(e);
}
});
});
}
saveBytesToFile 这里可以用 fileio / fs 写二进制文件,后面在“相册写入”里统一说明路径怎么选。
2.2 录像流程
录像一般会多一个 Recorder / VideoOutput,典型步骤是:
- 创建
VideoOutput,指定编码格式、分辨率、比特率等 - 将
VideoOutput加入CameraSession - 调用
start()开始录制 - 调用
stop()结束录制并生成文件
简化示例:
import media from '@ohos.multimedia.media';
interface CameraContextWithVideo extends CameraContext {
recorder: media.AVRecorder;
}
async function initVideo(ctx: CameraContext, videoPath: string): Promise<CameraContextWithVideo> {
const avProfile: media.AVRecorderConfig = {
audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV,
profile: {
fileFormat: media.ContainerFormatType.CONTAINER_FORMAT_MPEG_4,
videoCodec: media.CodecMimeType.VIDEO_AVC,
audioCodec: media.CodecMimeType.AUDIO_AAC,
width: 1280,
height: 720,
videoBitrate: 2_000_000,
videoFrameRate: 30
},
url: videoPath,
rotation: 0
};
const recorder = await media.createAVRecorder(avProfile);
const videoOutput = (await getCameraManager()).createVideoOutput({
surfaceId: recorder.getInputSurface()
});
await ctx.session.beginConfig();
await ctx.session.addOutput(videoOutput);
await ctx.session.commitConfig();
return {
...ctx,
recorder,
videoOutput
};
}
async function startRecording(ctx: CameraContextWithVideo) {
await ctx.recorder.start();
}
async function stopRecording(ctx: CameraContextWithVideo) {
await ctx.recorder.stop();
}
实际项目里你会多一层状态管理(录制中 / 空闲),并在 UI 上做按钮禁用等状态反馈。
三、相册写入:让系统相册也能看到你的照片/视频
拍照 / 录像后只是生成了文件,想让系统相册里出现,需要通过 MediaLibrary(媒体库) 注册。
3.1 常用媒体库能力
主要是 @ohos.multimedia.mediaLibrary:
- 创建媒体文件(图片 / 视频)
- 获取媒体文件的公有保存路径
- 写入二进制数据
- 通知系统刷新媒体库
3.2 保存照片到相册
import mediaLibrary from '@ohos.multimedia.mediaLibrary';
import fileio from '@ohos.fileio';
async function savePhotoToGallery(context, buffer: ArrayBuffer): Promise<string> {
const ml = mediaLibrary.getMediaLibrary(context);
// 1. 创建媒体资源
const photoAsset = await ml.createAsset(mediaLibrary.MediaType.IMAGE, 'IMG_' + Date.now() + '.jpg', 'Pictures');
// 2. 打开文件句柄
const fd = await ml.openAsset(photoAsset.id);
// 3. 写入
const writeBuffer = new Uint8Array(buffer);
await fileio.write(fd, writeBuffer);
// 4. 关闭
await fileio.close(fd);
return photoAsset.uri;
}
拍照回调里直接调用:
ctx.photoOutput.capture({}, async (err, photoData) => {
if (err) return;
await savePhotoToGallery(getContext(this), photoData.buffer);
// 可以 Toast 提示:保存成功
});
3.3 保存视频到相册
录像时你一般已经指定了 url(视频文件保存路径),结束录制之后,往媒体库注册即可:
async function saveVideoToGallery(context, filePath: string): Promise<string> {
const ml = mediaLibrary.getMediaLibrary(context);
const videoAsset = await ml.createAsset(
mediaLibrary.MediaType.VIDEO,
'VID_' + Date.now() + '.mp4',
'Videos'
);
const fd = await ml.openAsset(videoAsset.id);
// 把 filePath 对应的文件内容拷贝进来
const srcFd = await fileio.open(filePath, fileio.O_RDONLY);
const buffer = new ArrayBuffer(1024 * 1024);
let bytesRead = 0;
while ((bytesRead = await fileio.read(srcFd, buffer)) > 0) {
await fileio.write(fd, new Uint8Array(buffer, 0, bytesRead));
}
await fileio.close(srcFd);
await fileio.close(fd);
return videoAsset.uri;
}
简化起见实际可用更高效的文件拷贝方式,但思路一样:先有文件 → 媒体库建 Asset → 构建媒体文件 → 拷数据进去。
四、相机权限:不配好,前面所有代码都是摆设
相机 / 录像类应用必备的权限:
ohos.permission.CAMERA—— 使用摄像头ohos.permission.MICROPHONE—— 录像时录音ohos.permission.READ_MEDIA/ohos.permission.WRITE_MEDIA或相关存储权限 —— 写相册
4.1 module.json5 静态声明
{
"module": {
// ...
"requestPermissions": [
{ "name": "ohos.permission.CAMERA" },
{ "name": "ohos.permission.MICROPHONE" },
{ "name": "ohos.permission.READ_MEDIA" },
{ "name": "ohos.permission.WRITE_MEDIA" }
]
}
}
4.2 动态申请(和 BLE 那一篇套路一样)
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
const PERMISSIONS = [
'ohos.permission.CAMERA',
'ohos.permission.MICROPHONE',
'ohos.permission.READ_MEDIA',
'ohos.permission.WRITE_MEDIA'
];
export async function ensureCameraPermissions(context): Promise<boolean> {
const atManager = abilityAccessCtrl.createAtManager();
for (const p of PERMISSIONS) {
const grantStatus = await atManager.checkAccessToken(context.tokenId, p);
if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) continue;
const res = await atManager.requestPermissionsFromUser(context, [p]);
if (res.authResults[0] !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
console.error(`权限被拒绝: ${p}`);
return false;
}
}
return true;
}
建议在用户进入“相机页面”前调用:
const ctx = getContext(this);
const ok = await ensureCameraPermissions(ctx);
if (!ok) {
// 给出弹窗提示:不开权限没法用相机
return;
}
五、自定义相机 UI 实战:一屏搞定预览 + 拍照 + 录像
下面给你一个 简化版自定义相机页面 思路,包含:
- 上半屏预览(绑定 SurfaceId)
- 下半屏拍照按钮 + 录像按钮(带状态)
注意:真实运行需要与你当前 SDK 下的“相机预览组件”/Surface 绑定方式对齐,这里重点是UI & 状态组织方式。
5.1 ArkUI 结构示意
@Entry
@Component
struct CustomCameraPage {
private cameraCtx?: CameraContextWithVideo;
@State surfaceId: string = ''; // 预览用 Surface ID
@State isRecording: boolean = false;
async aboutToAppear() {
const ctx = getContext(this);
const ok = await ensureCameraPermissions(ctx);
if (!ok) return;
// 创建预览 Surface
// 实际可以用 SurfaceContainer / Texture / 相机专用组件获取 surfaceId
this.surfaceId = await createPreviewSurfaceId(); // 伪代码
const baseCtx = await createCameraContext(this.surfaceId);
this.cameraCtx = await initVideo(baseCtx, getTempVideoPath());
}
aboutToDisappear() {
// 清理:停止预览、释放会话等
}
async onTakePhoto() {
if (!this.cameraCtx) return;
// 采集一张照片,将 buffer 写入相册
this.cameraCtx.photoOutput.capture({}, async (err, photoData) => {
if (err) return;
await savePhotoToGallery(getContext(this), photoData.buffer);
// Toast:保存成功
});
}
async onRecordToggle() {
if (!this.cameraCtx) return;
if (!this.isRecording) {
await startRecording(this.cameraCtx);
this.isRecording = true;
} else {
await stopRecording(this.cameraCtx);
this.isRecording = false;
// 将临时文件写入相册
await saveVideoToGallery(getContext(this), getTempVideoPath());
}
}
build() {
Column() {
// 预览区域
// 这里放相机预览组件(示意)
CameraPreview({ surfaceId: this.surfaceId }) // 自封装组件
// 底部控制栏
Row() {
// 拍照按钮
Button('拍照')
.width(80).height(80)
.borderRadius(40)
.onClick(() => this.onTakePhoto())
// 录像按钮
Button(this.isRecording ? '停止' : '录像')
.width(80).height(80)
.borderRadius(40)
.margin({ left: 40 })
.backgroundColor(this.isRecording ? '#FF4444' : '#FFFFFF')
.onClick(() => this.onRecordToggle())
}
.justifyContent(FlexAlign.Center)
.margin({ top: 16, bottom: 32 })
}
.width('100%')
.height('100%')
.backgroundColor('#000000')
}
}
UI 可以随你发挥:圆环式快门、长按拍视频、加网格线、加缩略图、加切换镜头按钮等等,本质上都是在上面的逻辑基础上做交互层优化。
小结:把相机这条线过一遍,你就离“相机类 App”不远了
这一篇按你给的大纲,已经把主干打通了:
-
CameraX API / Camera 架构
- CameraManager → CameraInput → Output → Session → 预览 Surface
-
拍照 / 录像能力
PhotoOutput.capture()→ 获取 JPEG 数据AVRecorder + VideoOutput→ 控制开始 / 停止录制
-
相册写入
- MediaLibrary 创建图片 / 视频 Asset
- 写入二进制数据 → 系统相册可见
-
相机权限
CAMERA / MICROPHONE / READ_MEDIA / WRITE_MEDIA- 静态 + 动态权限申请
-
自定义相机 UI 实战
- 自己写预览 + 操作按钮
- 利用状态管理(isRecording 等)做 UI 反馈
如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~
更多推荐


所有评论(0)