当前位置:首页>鸿蒙APP>【鸿蒙】HarmonyOS 媒体能力深度实战:Camera/Audio/Video 开发全解析

【鸿蒙】HarmonyOS 媒体能力深度实战:Camera/Audio/Video 开发全解析

  • 2026-10-21 05:35:46
【鸿蒙】HarmonyOS 媒体能力深度实战:Camera/Audio/Video 开发全解析

HarmonyOS 媒体能力深度实战:Camera/Audio/Video 开发全解析

封面信息图

一文掌握 HarmonyOS NEXT 媒体三件套,避开 90% 开发者踩过的坑,从采集到播放全流程跑通。

适用版本:HarmonyOS NEXT / API 12+ 阅读时长:约 18 分钟


场景切入:为什么媒体开发这么难?

你写了一个拍照功能,真机跑起来黑屏;录音权限申请通过了但 AudioCapturer 始终报 -1;视频播放器在部分设备上卡顿、首帧慢——这些问题不是你代码写错了,而是 HarmonyOS 媒体 API 有严格的状态机约束,顺序错一步就全盘崩。

本文从实际开发场景出发,拆解 Camera Kit、Audio Kit 和 AVPlayer 的核心机制,给出可直接运行的 ArkTS 代码示例,并系统整理高频坑点。


一、Camera Kit:拍照与录像

1.1 架构总览

CameraManager    ├── getSupportedCameras()        → Array<CameraDevice>    ├── createCameraInput(device)    → CameraInput    └── createSession(sessionType)  → PhotoSession / VideoSessionPhotoSession    ├── addInput(cameraInput)    ├── addOutput(photoOutput)    ├── commitConfig()              ← 必须调用,否则配置不生效    └── start()PhotoOutput    └── capture(settings)           → 触发拍照

整体流程遵循严格状态机:IDLE → CONFIGURING → COMMITTED → STARTED → STOPPED,任何跨状态调用都会抛出 CameraError。

1.2 最简拍照实现

import { camera } from'@kit.CameraKit';import { fileIo } from'@kit.CoreFileKit';asyncfunctiontakeSinglePhoto(context: Context): Promise<string> {const cameraManager = camera.getCameraManager(context);const cameras = cameraManager.getSupportedCameras();// 选择后置摄像头const backCamera = cameras.find(c => c.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK  ) ?? cameras[0];// ❌ 错误写法:直接 start() 而不 commitConfig()// session.start(); // 报错:状态机不在 COMMITTED 状态// ✅ 正确写法:严格按顺序执行const cameraInput = cameraManager.createCameraInput(backCamera);await cameraInput.open();const photoProfile = cameraManager.getSupportedOutputCapability(    backCamera, camera.SceneMode.NORMAL_PHOTO  ).photoProfiles[0];const photoOutput = cameraManager.createPhotoOutput(photoProfile);const session = cameraManager.createSession<camera.PhotoSession>(    camera.SceneMode.NORMAL_PHOTO  );  session.beginConfig();  session.addInput(cameraInput);  session.addOutput(photoOutput);await session.commitConfig(); // 必须 await,提交配置await session.start();        // 启动预览// 拍照并保存const savePath = context.filesDir + `/photo_${Date.now()}.jpg`;const file = fileIo.openSync(savePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);  photoOutput.on('photoAvailable', (err, photo) => {const buffer = photo.main.getComponent(camera.Component.JPEG)!.byteBuffer;    fileIo.writeSync(file.fd, buffer);    fileIo.closeSync(file.fd);  });await photoOutput.capture(); // 触发拍摄return savePath;}

1.3 录像会话差异

录像使用 VideoSession,需要额外传入 VideoOutput 和 AVRecorder:

import { media } from'@kit.MediaKit';asyncfunctionstartVideoRecording(context: Context, savePath: string) {const avRecorder = await media.createAVRecorder();// AVRecorder 必须先 prepare,再绑定到 VideoOutputawait avRecorder.prepare({    videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV,    profile: {      fileFormat: media.ContainerFormatType.CFT_MPEG_4,      videoBitrate: 2000000,      videoCodec: media.CodecMimeType.VIDEO_AVC,      videoFrameWidth: 1280,      videoFrameHeight: 720,      videoFrameRate: 30    },    url: `fd://${fileIo.openSync(savePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY).fd}`  });const videoSurface = await avRecorder.getInputSurface();// 后续 addOutput(videoOutput) 时需传入 videoSurface 创建的 VideoOutput}

关键差异对比:

能力
PhotoSession
VideoSession
输出类型
PhotoOutput
VideoOutput + AVRecorder
触发采集
photoOutput.capture()
avRecorder.start()
停止
无需额外操作
avRecorder.stop() → avRecorder.reset()
Surface 绑定
不需要
必须在 addOutput 前获取 Surface

二、Audio Kit:录音与播放

2.1 核心类职责划分

AudioCapturer   → 麦克风采集 PCM 原始数据(实时处理场景)AudioRenderer   → 播放 PCM/AAC 原始数据(自定义播放场景)AVRecorder      → 高层封装,采集+编码+写文件(推荐录音场景)AVPlayer        → 高层封装,解码+渲染(推荐播放场景)

选型原则:

  • 需要对原始 PCM 做实时处理(如波形图、变声)→ AudioCapturer / AudioRenderer
  • 只需录音到文件 → AVRecorder
  • 只需播放音视频文件 → AVPlayer

2.2 AudioCapturer 采集实践

import { audio } from'@kit.AudioKit';asyncfunctionstartAudioCapture(): Promise<audio.AudioCapturer> {const streamInfo: audio.AudioStreamInfo = {    samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,    channels: audio.AudioChannel.CHANNEL_2,    sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,    encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW  };const capturerInfo: audio.AudioCapturerInfo = {    source: audio.SourceType.SOURCE_TYPE_MIC,    capturerFlags: 0  };const capturer = await audio.createAudioCapturer({ streamInfo, capturerInfo });// ❌ 错误写法:不等待 start() 完成就读取数据// capturer.read(bufferSize, true, callback); // 状态未就绪,返回空数据// ✅ 正确写法:等待 start 完成后再注册 readData 回调await capturer.start();  capturer.on('readData', (buffer: ArrayBuffer) => {// buffer 即为 PCM 原始数据,可直接写文件或实时处理console.log(`采集到 ${buffer.byteLength} 字节 PCM 数据`);  });return capturer;}asyncfunctionstopAudioCapture(capturer: audio.AudioCapturer) {await capturer.stop();await capturer.release(); // 必须 release,否则持续占用麦克风}

2.3 权限申请(常见漏项)

媒体开发必须在 module.json5 中声明权限,**API 12+ 还需填写 usedScene**,否则动态申请时权限无法弹窗:

// module.json5 中的 requestPermissions{  "name": "ohos.permission.MICROPHONE",  "reason": "$string:reason_microphone",  "usedScene": {    "abilities": ["EntryAbility"],    "when": "inuse"   // ← API 12 起必填,填 always 或 inuse  }}
import { abilityAccessCtrl, Permissions } from'@kit.AbilityKit';asyncfunctionrequestMediaPermissions(context: Context): Promise<boolean> {const permissions: Permissions[] = ['ohos.permission.MICROPHONE','ohos.permission.CAMERA'  ];const atManager = abilityAccessCtrl.createAtManager();const result = await atManager.requestPermissionsFromUser(context, permissions);return result.authResults.every(r => r === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED);}

三、AVPlayer:视频播放深度解析

3.1 状态机全景

idle  ↓ url = '...'(赋值触发)initialized  ↓ prepare()prepared  ←─────────────────────┐  ↓ play()                      │ reset()playing                          │  ↓ pause()                     │paused ──→ play() ──→ playing   │  ↓ stop()                      │stopped ─────────────────────────┘  ↓ reset()idle

跨状态调用是最高频的崩溃来源。 在 initialized 状态调用 play() 直接报状态机错误。

3.2 完整播放器实现

import { media } from'@kit.MediaKit';@Componentstruct VideoPlayer {private avPlayer: media.AVPlayer | null = null;@State isPlaying: boolean = false;private surfaceId: string = '';async initPlayer(url: string) {this.avPlayer = await media.createAVPlayer();// 注册 stateChange 回调(必须在 url 赋值前注册)this.avPlayer.on('stateChange', async (state: string) => {switch (state) {case'initialized':// ✅ initialized 后才能调用 prepare,不能直接 playawaitthis.avPlayer!.prepare();break;case'prepared':// prepared 后设置 surfaceId,再 playthis.avPlayer!.surfaceId = this.surfaceId;awaitthis.avPlayer!.play();this.isPlaying = true;break;case'completed':awaitthis.avPlayer!.seek(0, media.SeekMode.SEEK_PREV_SYNC);break;case'error':awaitthis.avPlayer!.reset();break;      }    });this.avPlayer.on('error', (err) => {console.error(`AVPlayer error: ${err.code} - ${err.message}`);    });// url 赋值触发状态 idle → initializedthis.avPlayer.url = url;  }async releasePlayer() {if (this.avPlayer) {awaitthis.avPlayer.stop();awaitthis.avPlayer.reset();awaitthis.avPlayer.release(); // 释放解码器等系统资源this.avPlayer = null;    }  }  build() {    Column() {      XComponent({        id: 'videoSurface',type: XComponentType.SURFACE,  // ← 必须是 SURFACE,不能是 COMPONENT        controller: new XComponentController()      })        .onLoad((controller) => {// surfaceId 在 onLoad 回调后才可用this.surfaceId = controller.getXComponentSurfaceId();        })        .width('100%')        .aspectRatio(16 / 9)      Button(this.isPlaying ? '暂停' : '播放')        .onClick(async () => {if (!this.avPlayer) return;if (this.isPlaying) {awaitthis.avPlayer.pause();this.isPlaying = false;          } else {awaitthis.avPlayer.play();this.isPlaying = true;          }        })    }  }}

3.3 seek 精度控制

// ❌ 错误写法:单参数 seek,默认 SEEK_PREV_SYNC,可能与期望位置偏差数秒await avPlayer.seek(30000);// ✅ 正确写法:明确传入精度模式await avPlayer.seek(30000, media.SeekMode.SEEK_CLOSEST);     // 帧精确,适合编辑场景await avPlayer.seek(30000, media.SeekMode.SEEK_NEXT_SYNC);   // 关键帧对齐,适合快进场景

四、最佳实践

4.1 及时释放媒体资源

做法: 在 aboutToDisappear 或 onPageHide 中主动调用 release()。

原因: 摄像头、麦克风、硬件解码器是独占型系统资源,页面切走后若不释放,其他应用无法获取,同时自身下次打开也会冲突。

不这样做: 退出页面后摄像头指示灯常亮,再次进入拍照页 cameraInput.open() 报 CONFLICT_CAMERA(错误码 7400101)。

4.2 在 stateChange 回调中驱动 AVPlayer

做法: 所有状态迁移操作(prepare()、play()、seek())放在对应的 stateChange 回调里,而非在赋值 url 后直接链式调用。

原因: AVPlayer 是异步状态机,url 赋值返回不代表状态已切换到 initialized,直接调用下一步会触发非法状态错误。

不这样做: 随机出现 state machine error,仅在低端设备(异步切换慢)上复现,难以稳定复现和排查。

4.3 录音前申请焦点并监听焦点变化

做法: 使用 AudioSessionManager 申请音频焦点,并注册 audioInterrupt 事件监听,失去焦点时暂停采集。

原因: 来电、系统通知等会抢占音频焦点,继续采集会导致录音内容混入系统声音,或采集到静音数据。

不这样做: 用户接打电话期间录音文件包含通话内容,存在隐私合规风险。


五、常见坑点

坑 1:预览画面黑屏

  • 现象: Camera 所有 API 返回成功,XComponent 上无画面显示。
  • 原因:session.start() 在 XComponent.onLoad 之前调用,surfaceId 尚未绑定到 PreviewOutput。
  • 复现: 在 aboutToAppear 中初始化 Camera,而不是在 XComponent.onLoad 回调中。
  • 解决: 将整个 Camera 初始化链路(createCameraInput → addOutput → commitConfig → start)移入 XComponent.onLoad 回调,确保 surfaceId 已就绪。

坑 2:AudioCapturer.start() 报 -1(SYSTEM_ERROR)

  • 现象: 动态权限弹窗已授权,但 capturer.start() 抛出错误码 -1。
  • 原因:module.json5 中 requestPermissions 缺少 usedScene.when 字段,API 12 起该字段必填,缺失时权限实际未生效。
  • 复现: 旧工程迁移到 API 12 后直接运行,未更新权限配置。
  • 解决: 在 module.json5 中补全 "usedScene": {"abilities": ["EntryAbility"], "when": "inuse"}。

坑 3:AVPlayer 有声音无画面

  • 现象: 音频正常播放,视频区域黑屏。
  • 原因: XComponent 的 type 设置为 COMPONENT 而非 SURFACE,或 surfaceId 在 prepared 状态前未赋值给 avPlayer.surfaceId。
  • 复现: XComponent 类型写错,或在 Button 点击后才赋值 surfaceId。
  • 解决: 确认 type: XComponentType.SURFACE,并在 stateChange === 'prepared' 回调中、play() 调用前赋值 avPlayer.surfaceId。

六、总结

  1. Camera、Audio、AVPlayer 均为严格状态机,调用顺序错一步即报错。
  2. CameraSession.commitConfig() 是 Camera 配置生效的关键节点,不可省略。
  3. AVPlayer 所有状态迁移操作必须在 stateChange 回调中触发,禁止赋值后直接链式调用。
  4. 媒体资源独占,页面退出必须显式调用 release(),否则导致资源冲突。
  5. API 12+ 权限申请必须填写 usedScene.when,旧工程迁移需重点检查。

核心结论:HarmonyOS 媒体开发的本质是状态机驱动的资源管理,调用顺序和及时释放比业务逻辑本身更重要。


参考资料

  • Camera Kit 开发指南
  • AVPlayer 开发指南
  • Audio Kit 录音开发
  • OpenHarmony 源码路径:foundation/multimedia/camera_framework/ 、foundation/multimedia/av_session/

最新文章

随机文章