【鸿蒙】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├── 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;async function takeSinglePhoto(context: Context): Promise {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.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和AVRecorderimport { media } from kit.MediaKit;async function startVideoRecording(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- 只需播放音视频文件 →AVPlayer2.2 AudioCapturer 采集实践import { audio } from kit.AudioKit;async function startAudioCapture(): Promise {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;}async function stopAudioCapture(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;async function requestMediaPermissions(context: Context): Promise {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不能直接 playawait this.avPlayer!.prepare();break;case prepared:// prepared 后设置 surfaceId再 playthis.avPlayer!.surfaceId this.surfaceId;await this.avPlayer!.play();this.isPlaying true;break;case completed:await this.avPlayer!.seek(0, media.SeekMode.SEEK_PREV_SYNC);break;case error:await this.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) {await this.avPlayer.stop();await this.avPlayer.reset();await this.avPlayer.release(); // 释放解码器等系统资源this.avPlayer null;}}build() {Column() {XComponent({id: videoSurface,type: XComponentType.SURFACE, // ← 必须是 SURFACE不能是 COMPONENTcontroller: 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) {await this.avPlayer.pause();this.isPlaying false;} else {await this.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 已就绪。坑 2AudioCapturer.start() 报 -1SYSTEM_ERROR-现象动态权限弹窗已授权但capturer.start()抛出错误码 -1。-原因module.json5中requestPermissions缺少usedScene.when字段API 12 起该字段必填缺失时权限实际未生效。-复现旧工程迁移到 API 12 后直接运行未更新权限配置。-解决在module.json5中补全usedScene: {abilities: [EntryAbility], when: inuse}。坑 3AVPlayer 有声音无画面-现象音频正常播放视频区域黑屏。-原因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/