在这里插入图片描述

一、我们要做什么

随着鸿蒙 Next 正式去掉 AOSP 代码、全面转向自研微内核与方舟编译器,媒体类应用的开发范式也随之改变。过去依赖 Android MediaPlayer、ExoPlayer 或 ffmpeg 的链路在鸿蒙上需要重新映射到 HarmonyOS 的媒体 Kit 体系。本节先明确我们要构建的目标应用,以及它在真实业务中必须覆盖的媒体需求。

1.1 媒体需求表

需求编号 功能描述 使用能力 关键约束
N-01 网络/本地音视频播放 AVPlayer 支持 seek、倍速、音量
N-02 麦克风+摄像头录制 AVRecorder 30fps、H.264、AAC
N-03 图片裁剪/缩放/格式转换 Image Kit 单边最大 4096px
N-04 低延迟 PCM 渲染 AudioRenderer 48kHz/16bit 双声道
N-05 麦克风采集 AudioCapturer 双声道、48kHz
N-06 后台媒体键接管 AVSession 锁屏/通知栏控制

1.2 预期效果

  • 用户打开应用即可播放一段示例视频,进度条、倍速、音量均受控;
  • 点击录制后,生成的 MP4 文件可立即回放;
  • 图片编辑页支持中心裁剪并导出 JPEG;
  • 应用退到后台后,系统媒体中心仍显示播放状态,耳机/锁屏键可以控制播放与暂停;
  • 自定义音频渲染器能够把一段 PCM 缓冲实时写出到扬声器。

1.3 工程挑战

  1. 状态机复杂:AVPlayer 从 idle→initialized→prepared→playing 的跃迁依赖异步回调,若跳过 prepare 直接 play 会抛异常。
  2. 资源释放:fd、PixelMap、AudioRenderer、AVRecorder 如不主动 release,会造成文件句柄与内存泄漏。
  3. 权限模型:麦克风、摄像头、媒体读取需在 module.json5 声明并在运行时动态申请。
  4. 硬解兼容:不同芯片平台的硬解能力差异较大,需用软解兜底。
  5. 会话抢占:多应用同时播放时,只有激活的 AVSession 才能接管系统媒体键。

1.4 架构总览与模块边界

本应用的模块边界遵循“能力分层”原则:UI 层(ArkUI 页面)只负责展示与交互,不直接调用媒体引擎;逻辑层(Controller 与 Session 封装)托管 AVPlayer、AVRecorder、AudioRenderer、AVSession 的生命周期与错误恢复;系统层由 HarmonyOS 媒体服务(Media Service)统一托管解封装、解码、混音与硬件调度。三层之间用事件总线(EventHub 或 AppStorage)通信,避免页面因媒体回调频繁刷新状态造成掉帧。

在端到端链路中,播放链路为“用户点击 → PlayerController.init(url) → 媒体服务解封装解码 → 渲染到 XComponent”;录制链路为“Camera 采集 Surface → AVRecorder 编码封装 → fd 落盘 → 生成 MediaAsset”;图片链路为“相册选择 → ImageSource 解码 → PixelMap 编辑 → ImagePacker 编码”;音频链路为“AudioCapturer 采集 / AudioRenderer 渲染”;会话链路为“AVSession 激活 → 系统媒体中心订阅”。四条链路最终都收敛到 AVSession,保证后台体验一致。

之所以把录制与播放拆成独立模块,是为了契合“单一职责”:AVRecorder 在录制时独占麦克风与摄像头,若与播放同页容易出现音频焦点冲突。拆分后用户心智更清晰,也便于后续接入画中画与跨端流转。

在测试矩阵上,我们覆盖三类设备:旗舰手机(验证 4K 硬解与高码率录制)、中端平板(验证软解兜底与功耗)、车机与智慧屏(验证媒体会话流转)。不同设备的硬解能力差异是主要回归点,因此 CI 必须包含“播放异常码捕获”与“录制 profile 协商”两条用例,并在低端机型上验证内存与温度表现。

1.5 权限与合规清单

媒体应用涉及用户隐私,必须在 module.json5 中声明权限并在运行时用 abilityContext.requestPermissionsFromUser 动态申请:ohos.permission.MICROPHONE(录制音频)、ohos.permission.CAMERA(录制视频)、ohos.permission.READ_MEDIA(读取相册)、ohos.permission.INTERNET(播放网络流)。在鸿蒙 Next 的隐私模型下,麦克风/摄像头每次进入录制页都应重新弹窗确认,且要在 UI 上给出明确的采集指示,告知用户正在录制,避免隐私合规风险。

1.6 开发环境与版本基线

本文示例基于 HarmonyOS NEXT API 12 与 DevEco Studio 5.x,使用 Stage 模型与 ArkTS 严格模式。媒体相关 Kit 在 API 9 起逐步稳定,AVSession 在 API 10 后提供了完整的媒体键回调;若需兼容更早版本,需注意 createAVPlayer 的回调签名差异。真机调试建议开启“USB 调试”与“媒体权限沙盒”,并在 build-profile.json5 中打开 strictMode。项目依赖仅需 @kit 系列官方包,无需引入额外三方库,这也是后续包体积优势的来源。

1.7 验收与上线前检查

上线前需确认:所有媒体权限均在首次使用时申请并向用户说明用途;后台播放已声明长时任务;AVSession 在锁屏与控制中心显示正确元数据;录制产物可在相册正常回放;异常路径(断网、无麦克风权限)有友好提示。建议准备一份媒体回归清单,覆盖各分辨率与编码组合。

1.8 示例代码约定

为聚焦核心逻辑,全文示例省略了部分非空判断与日志埋点,实际工程中请补全。所有 Controller 均建议以单例或 Ability 级上下文持有,避免被频繁创建销毁。涉及 fd 的操作务必成对调用 open/close,防止句柄泄漏。

1.9 与系统主题的协同

媒体应用应尊重系统深色模式与字体缩放,播放控件用 ArkUI 的 @Style 与资源限定符适配。锁屏媒体卡片的封面需提供明暗两套,避免在深色背景下出现亮边。这样媒体能力与系统 UX 语言保持一致,用户体感更原生。

1.10 性能基线指标

我们为媒体应用设定基线:冷启到可交互 < 400ms,播放首帧 < 1s,录制启动 < 300ms,内存峰值 < 200MB。任何提交都应对照基线,防止性能回归。

1.11 灰度与回滚

媒体能力随系统版本持续演进,发布宜采用灰度策略:先小比例放量,监控硬解失败率、播放 ANR 与录制异常率,指标异常升高即回滚并定位根因。把媒体关键指标纳入核心看板,是线上稳定的重要保障。

1.12 国际化与文案

媒体控件文案需随系统语言切换,错误提示提供多语言资源;时间格式与时长显示遵循区域习惯。出海应用尤其要注意右到左布局下控件镜像,避免播放/暂停图标位置错乱。

1.13 安全与隐私

录制产生的音视频属于敏感数据,应默认存于应用沙箱并加密;分享前明确告知用户,并提供本地删除能力。麦克风/摄像头指示灯必须按系统规范展示,杜绝静默采集。


二、数据模型设计

在动手写业务代码前,先用 TypeScript interface 把“媒体资源”“播放状态机”“录制任务”三类核心实体固化下来,保证 UI 层与媒体层解耦。

// 媒体资源描述
export interface MediaAsset {
  id: string;
  title: string;
  url: string;
  mimeType: 'video/mp4' | 'audio/mpeg' | 'image/jpeg';
  durationMs?: number;
  sizeBytes: number;
  localPath?: string;
}

// 播放状态机(与 AVPlayer 内部状态对齐)
export type PlayerState =
  | 'idle' | 'initialized' | 'prepared'
  | 'playing' | 'paused' | 'stopped' | 'released' | 'error';

// 统一媒体会话控制指令
export type MediaCommand =
  | 'play' | 'pause' | 'stop' | 'next' | 'previous' | 'seek';

// 录制任务状态
export interface RecordTask {
  assetId: string;
  fd: number;
  startedAt: number;
  status: 'recording' | 'paused' | 'finished';
}

2.1 媒体资源模型

MediaAsset 是播放列表与文件管理的最小单元。url 既可是 https:// 网络地址,也可是 fd://<num> 文件句柄地址;mimeType 用于驱动 UI 选择 AVPlayer 还是 Image Kit。localPath 在离线缓存时由下载模块回填。

2.2 播放状态机模型

PlayerStatemedia.AVPlayer 暴露的 stateChange 事件一一对应。我们把 releasederror 补进枚举,便于 UI 在异常分支统一收口。状态跃迁规则:初始化时 idle,设置 url 后进入 initializedprepare() 成功进入 prepared,调用 play() 进入 playingpause() 回到 pausedrelease() 进入 released。任何回调中的异常都会落入 error

2.3 录制任务模型

RecordTask 关联一个文件描述符 fd 与生命周期状态。startedAt 用于计算录制时长;status 由 AVRecorder 的 start/pause/resume/stop 驱动,配合 UI 的计时器展示实时时长。

2.4 状态机与 UI 绑定

数据模型定义好后,UI 通过订阅 PlayerState 驱动界面。一种推荐模式是把状态写入 AppStorage,页面用 @StorageLink 读取,这样播放器回调不直接触碰组件实例,避免页面销毁后回调空引用。录制计时同样基于 RecordTask.startedAtsetInterval 计算,中断时写回 status,恢复时续算。

对于图片编辑,我们额外定义一个 EditPreset 接口描述裁剪比例、目标宽高与质量,使 editPhoto 可被不同页面复用,而无需重复传参。统一模型让测试也能针对纯数据做断言,不依赖真实媒体文件,从而把媒体逻辑与 UI 渲染彻底解耦,提升单测覆盖率与重构安全性。

2.5 媒体会话元数据模型

除了资源与状态,AVSession 还需要一组元数据来渲染锁屏卡片,包括 assetId、标题、艺术家、专辑、时长与封面 URI。封面建议预缩放至 512×512 以内以减少内存。播放进度由独立的 PlaybackPosition 结构维护,UI 每 500ms 轮询一次,避免高频刷新。该元数据模型与 MediaAsset 解耦,使同一首歌在播放、缓存、分享三种场景下都能复用同一份信息,降低多模块间的同步成本。

2.6 数据模型的测试策略

纯数据模型的最大收益是可测性。我们可以脱离真机,用单元测试校验状态跃迁的合法性:例如断言 idle 不能直接跳到 playingreleased 后不可再 play。录制任务的时长计算也可基于 startedAt 用假时钟验证,无需真实麦克风。这种“逻辑与媒体解耦”的写法,让回归测试稳定且快速。

2.7 与持久化层的关系

MediaAsset 应可序列化进关系型或键值数据库,作为播放历史与收藏的基础。我们在数据库层为其建立索引(id、mimeType、本地路径),使首页列表与“最近播放”都能直接复用同一模型,避免多份冗余缓存导致的数据不一致。

2.8 资源回收时序

模型层不直接持有 fd,而是记录其生命周期由 Controller 管理。页面 aboutToDisappear 时通知 Controller 释放;Ability onDestroy 时统一 releaseAll。清晰的“谁创建谁释放”边界,是避免媒体类内存泄漏的关键。

2.9 多实例隔离

同一进程内若同时存在播放与录制,应各自独立持有实例与状态,禁止共享 AVPlayer/AVRecorder,否则状态互相踩踏会导致不可预期的行为。

2.10 模型版本演进

MediaAsset 应带 schema 版本号,数据库迁移时兼容旧字段,避免应用升级后历史播放记录丢失。新能力以可选字段承载,保持向后兼容;用户数据独立存储,降低与媒体元数据的耦合。

2.11 与分布式数据同步

多设备场景下,MediaAsset 可通过分布式数据库在手机与平板间同步进度与收藏,配合 5.9 节的会话流转,实现“在 A 设备暂停、B 设备续播”的无缝体验,模型需携带 deviceId 与 lastPosition。

2.12 测试数据工厂

为单测构造 MediaAsset/RecordTask 的假数据工厂,覆盖边界(零时长、超大文件、非法 mimeType),让模型校验逻辑在无真机环境也能充分验证。


三、核心设计决策

在媒体方案上,开发者常面临“原生媒体 Kit vs 三方 SDK”“AVPlayer vs AVRecorder 分工”“软解 vs 硬解”三类抉择。下表给出对比与结论。

维度 方案 A 方案 B 选型结论
播放能力 原生 AVPlayer 三方 ExoPlayer 移植 选 A:原生对齐系统媒体会话、功耗更优
录制能力 原生 AVRecorder 自搭 Camera+编码器 选 A:AVRecorder 已封装音视频_mux
图片处理 Image Kit Canvas 手绘 选 A:Native 加速、API 简洁
解码路径 硬解优先 强制软解 混合:硬解失败回退软解
音频输出 AudioRenderer 直接写流 选 A:统一音量/焦点管理

3.1 AVPlayer vs AVRecorder

两者同属 @kit.MediaKit,但职责正交:AVPlayer 只负责“解封装+解码+渲染”的消费侧;AVRecorder 负责“采集+编码+封装”的生产侧。它们不能互替——不能用播放器录屏,也不能用录制器播文件。工程上我们把二者封装成独立的 PlayerControllerRecorderController,避免状态互相污染。

3.2 原生媒体 vs 三方 SDK

三方播放器 SDK 在鸿蒙上往往依赖兼容层,会损失系统级媒体会话、低功耗后台播放与版权保护(DRM)能力。原生 AVPlayer 直接对接媒体引擎(Media Service),能复用系统硬解与音频焦点策略,因此默认优先原生;仅在需要特殊协议(如私有 P2P 流)时再引入三方。

3.3 硬解与软解

鸿蒙媒体引擎默认走硬件解码器(通过 Codec HDI 对接芯片 VPU)。当遇到非常规编码或硬解返回 AVError 时,应捕获错误并切换到软件解码路径。本示例以硬解为主、错误兜底为设计基调,不显式切换软解以避免复杂度,但保留了 error 回调的扩展点,用于上报埋点与触发软解兜底策略。

3.4 性能与功耗权衡

原生媒体 Kit 的另一大优势是功耗。AVPlayer 的硬解路径把解码卸载到 VPU,CPU 占用通常低于软件方案的 20%,在长视频场景下可显著延长续航。AVRecorder 同样走硬件编码,录制 1080p30 的整机功耗远小于自搭编码管线。

在启动性能上,首次 createAVPlayer 会触发媒体服务进程冷启,约几十毫秒;后续复用同一服务则几乎无感。因此工程中建议“延迟创建、提前复用”:进入播放页即创建实例并预置 url,但真正 play 延后到用户点击,既缩短首帧又能避免空转耗电。

关于三方 SDK,除非涉及私有协议或特殊滤镜,否则不建议引入,因为它们难以复用系统的低功耗后台播放与 DRM,长期维护成本更高。对于绝大多数业务,原生媒体栈已是兼顾性能、功耗与开发效率的最优解。

3.5 错误码与降级策略

媒体操作会返回 AVError 体系错误码,常见如 5400102(URL 无效)、5400103(数据源不可用)、5400106(解码失败)。工程上应建立“错误→动作”映射:网络类错误触发重试与降码率;解码类错误在允许时切换软解;资源类错误提示用户检查文件。所有错误都要回到 idle 态再重建,避免实例半死不活。

3.6 包体积与依赖权衡

原生 Kit 以 @kit 形式随系统分发,不增加应用包体积,这是相较三方 SDK 的隐性优势。若引入 ExoPlayer 等,往往带来数 MB 的 so 与 dex,拖慢安装与冷启。在出海或低端机占比高的场景,这一差异会直接体现在转化率与留存上。

3.7 决策小结

综上,默认路径是“原生媒体 Kit 全覆盖 + 硬解优先 + 错误软解兜底 + AVSession 统一接管”。仅当遇到私有协议、特殊滤镜或端到端加密等原生暂不支持的能力时,才局部引入三方组件,并严格隔离其生命周期,防止污染主媒体链路。这一策略在性能、功耗、包体积与可维护性上取得平衡。

3.8 录制参数选型

录制 profile 的选择直接决定产物体积与兼容性。音频优先 AAC(通用且体积小),视频优先 H.264(兼容性最佳),分辨率取设备与业务需求的较小值,帧率 30 已满足绝大多数场景。若面向高清分享,可提升到 1080p,但需评估中端机的编码温度。容器统一用 MP4,便于跨平台播放与编辑。

3.9 图片处理方案对比

Image Kit 提供声明式的高效编辑,底层走 Native 加速,适合裁剪、缩放、旋转、格式转换等常规操作;若需要逐像素滤镜或复杂合成,可结合 Canvas 或 NDK 图像库。一般而言,优先用 Image Kit 完成 80% 的编辑需求,仅在特殊效果上引入更高级方案,兼顾开发效率与性能。

3.10 缓存与预加载

对于列表型播放,可在 Wi-Fi 下预拉取下一集的元数据与首段,进入播放页时直接 prepare,显著缩短首帧。但需评估流量与存储,提供“仅 Wi-Fi 预加载”开关,尊重用户资费。

3.11 清晰度自适应

结合 5.5 节的埋点,根据实时带宽在 480p/720p/1080p 间切换,切换时通过 AVPlayer 的 seek 或重新 prepare 平滑过渡。自适应策略应保守,避免频繁跳动引发观感抖动。

3.12 无障碍支持

媒体控件需提供内容描述与焦点顺序,进度条支持语音播报;字幕渲染走系统无障碍服务。录制场景应给出闪光/振动提示替代声音提示,照顾听障用户。无障碍是上架与口碑的加分项。

3.13 软硬解切换代价

切换软解会显著增加功耗与发热,仅在硬解明确不支持时启用,并同步限制分辨率。工程上可缓存“设备-编码”支持表,在发起播放前就跳过必然失败的尝试,减少用户等待。

3.14 缩略图策略

列表封面用 Image Kit 解码出小尺寸缩略图并缓存到磁盘,避免每次滚动都解码原图,这是媒体列表流畅度的关键优化点,也能明显降低内存峰值。

3.15 多档位清晰度

为不同网络与机型预设多档清晰度(流畅/高清/超清),由 3.11 节的自适应策略动态选择,并把用户手动选择持久化,避免每次进入都重新协商,提升可控感。

3.16 字幕与多音轨

AVPlayer 支持外挂与内嵌字幕、多音轨切换;国际化视频可在字幕与音轨选择接口中切换语言,UI 提供轨道菜单,满足多语言观众需求。


四、完整代码实现

本章给出五个可运行的核心模块,每个代码块展示最关键的逻辑,控制在 50 行以内。

4.1 AVPlayer 状态机与播放控制

import { media } from '@kit.MediaKit';
import { common } from '@kit.AbilityKit';

export class PlayerController {
  private avPlayer: media.AVPlayer | undefined = undefined;
  private state: 'idle' | 'initialized' | 'prepared' | 'playing' | 'paused' | 'stopped' = 'idle';

  async init(context: common.UIAbilityContext, url: string) {
    this.avPlayer = await media.createAVPlayer();
    this.bindEvents();
    this.avPlayer.url = url; // 触发 initialized
  }

  private bindEvents() {
    this.avPlayer?.on('stateChange', (s, reason) => {
      this.state = s as any;
      if (s === 'initialized') this.avPlayer?.prepare();
      if (s === 'prepared') this.avPlayer?.play();
    });
    this.avPlayer?.on('error', (e) => console.error('player error', e));
  }

  play() { this.avPlayer?.play(); }
  pause() { this.avPlayer?.pause(); }
  seek(ms: number) { this.avPlayer?.seek(ms, media.SeekMode.SEEK_PREV_SYNC); }
  setRate(r: number) { this.avPlayer?.setSpeedRate(r); }
  setVolume(v: number) { if (this.avPlayer) this.avPlayer.volume = v; }

  release() {
    this.avPlayer?.release();
    this.avPlayer = undefined;
    this.state = 'idle';
  }
}

init 中设置 url 是异步触发点:引擎在后台完成解封装后会回调 initialized,此时再 prepare(),准备好后回调 prepared,我们自动 play()。所有控制方法均做空安全保护,避免对象已释放时崩溃。

4.2 AVRecorder 录制

import { media } from '@kit.MediaKit';

export async function startRecord(outputFd: number) {
  const recorder = await media.createAVRecorder();
  const config: media.AVRecorderConfig = {
    audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
    videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV,
    profile: {
      audioBitrate: 48000,
      audioChannels: 2,
      audioCodec: media.CodecMimeType.AUDIO_AAC,
      audioSampleRate: 48000,
      fileFormat: media.ContainerFormatType.CFT_MPEG_4,
      videoBitrate: 2000000,
      videoCodec: media.CodecMimeType.VIDEO_AVC,
      videoFrameWidth: 1280,
      videoFrameHeight: 720,
      videoFrameRate: 30,
    },
    url: `fd://${outputFd}`,
    rotation: 0,
  };
  await recorder.prepare(config);
  await recorder.start();
  return recorder;
}

录制的 url 必须是 fd:// 形式,因此上层需先通过 fileIo 打开输出文件拿到 fdprofile 中音视频编码、分辨率、帧率需与摄像头能力匹配,否则 prepare 会失败。视频画面来自 Surface,录制器与 Camera Kit 通过 Surface 对接。

实际工程中,outputFd 来自 fileIo.open 的返回值,录制结束后务必 fileIo.close(fd)recorder.release(),否则文件可能因缓冲未刷盘而损坏。若需边录边传,可在 start 后用 getAVRecorderProfile 校验实际参数。

4.3 Image Kit 图片编辑

import { image } from '@kit.ImageKit';
import { fileIo } from '@kit.CoreFileKit';

export async function editPhoto(path: string) {
  const file = fileIo.openSync(path, fileIo.OpenMode.READ_ONLY);
  const source = image.createImageSource(file.fd);
  const pixel = await source.createPixelMap();

  const info = await pixel.getImageInfo();
  const size = Math.min(info.size.width, info.size.height);
  const region: image.PositionArea = {
    x: (info.size.width - size) / 2,
    y: (info.size.height - size) / 2,
    width: size,
    height: size,
  };
  await pixel.crop(region);
  await pixel.scale(0.5, 0.5);

  const packer = image.createImagePacker();
  const buffer = await packer.packing(pixel, { format: 'image/jpeg', quality: 90 });
  return buffer;
}

createImageSource 从文件 fd 创建解码源,createPixelMap 拿到可编辑的像素对象。crop 按中心正方形裁剪,scale 等比缩放到 50%,最后用 ImagePacker 编码为 JPEG 字节流,可直接写回文件或上传。

对于批量处理,应把 createImageSourcecreatePixelMap 放在循环外复用,仅对每张图执行 crop/scale/packing。注意 ImagePacker 支持 bufferfile 两种输出,写入文件用 packingToFile 可直接落盘,省去手动 write

4.4 AudioRenderer 音频渲染

import { audio } from '@kit.AudioKit';

export async function renderPcm(pcm: ArrayBuffer) {
  const streamInfo: audio.AudioStreamInfo = {
    samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000,
    channels: audio.AudioChannel.CHANNEL_2,
    sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
    encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW,
  };
  const rendererInfo: audio.AudioRendererInfo = {
    usage: audio.StreamUsage.STREAM_USAGE_MEDIA,
    rendererFlags: 0,
  };
  const renderer = await audio.createAudioRenderer({ streamInfo, rendererInfo });
  await renderer.start();
  const writer = await renderer.getWriteBufferQueue();
  const buf = new Uint8Array(pcm);
  await writer.writeToQueue(buf);
  await renderer.stop();
  await renderer.release();
}

AudioRenderer 适合需要逐帧合成音效的场景(如游戏音、TTS)。streamInfo 必须与实际 PCM 格式一致,否则出现爆音或无声。getWriteBufferQueue 返回的写入器支持按帧投递,配合环形缓冲即可实现低延迟输出。对应的采集侧 AudioCapturer 构造方式相同,只是把 createAudioRenderer 换成 createAudioCapturer 并读取 ReadBufferQueue

双向语音场景需同时持有两者,并处理好回采回声消除(AEC)。采集到的 PCM 经降噪、增益后,再交给 AudioRenderer 播放,形成实时音频闭环。

4.5 媒体会话接管

import { avSession } from '@kit.AVSessionKit';

export async function buildSession(context: any) {
  const session = await avSession.createAVSession(context, 'demo', 'audio');
  await session.activate();

  await session.setAVMetadata({
    assetId: 'track-1',
    title: '鸿蒙媒体演示',
    artist: 'AtomGit',
    duration: 210000,
  });
  await session.setAVPlaybackState({
    state: avSession.PlaybackState.PLAYBACK_STATE_PLAY,
    position: { durationTime: 210000, elapsedTime: 0, updateTime: Date.now() },
  });

  session.on('play', () => console.info('系统媒体键: 播放'));
  session.on('pause', () => console.info('系统媒体键: 暂停'));
  session.on('playNext', () => console.info('系统媒体键: 下一首'));
  return session;
}

createAVSession 创建会话后必须 activate() 才能抢占系统媒体键。setAVMetadata/setAVPlaybackState 会同步到锁屏与控制中心。session.on('play'|'pause'|...) 订阅来自耳机、蓝牙、通知栏的指令,实现“后台也能被控制”。应用退出前台时不应 destroy,以便继续响应媒体键。

4.6 模块串联与页面装配

把上述五个控制器组合进一个 MediaManager 单例,对外暴露统一方法,页面只依赖该单例,降低耦合:

  • play(asset):内部调用 PlayerController.init 并在 prepared 后自动 play,同时把元数据同步给 AVSession
  • record():申请权限后 fileIo.open 拿到 fd,调用 startRecord,并把 RecordTask 写入状态;
  • edit():读取相册选择结果,调用 editPhoto 并回写 MediaAsset.localPath
  • renderPcm():把合成音效投递到 AudioRenderer
  • 媒体键:在 AbilityonCreatebuildSession,把 sessionplay/pause 事件桥接到 PlayerController,实现“锁屏也能控”。

装配时有一个关键时序:必须在 PlayerController 进入 playing 后才把 AVSession 状态置为 PLAYBACK_STATE_PLAY,否则系统媒体中心会显示“播放中”却无声音,造成状态不一致。错误恢复方面,任何媒体回调中的 error 都应先 release 再重建实例,而不是在损坏状态下继续调用。

此外,所有 Controller 都应提供 releaseAll(),在 Ability.onDestroy 中统一释放 fd、PixelMap、Renderer 与会话,杜绝资源泄漏。这样即使页面被系统回收,底层媒体资源也能干净退出,避免后台进程持有麦克风或摄像头引发的隐私合规问题。

4.7 权限校验与异常兜底

在调用 AVRecorder 前必须先校验麦克风与摄像头权限;缺失时引导用户去设置页开启。所有媒体调用都应包裹 try/catch,在 catch 中执行对应 Controller 的 release,并通过事件总线通知 UI 弹出错误提示。对于 AVPlayer,建议在 error 回调里记录错误码并自动重建实例,最多重试两次,避免单次网络抖动导致播放中断影响体验。

4.8 线程与调度注意

媒体回调默认在系统媒体线程,不应在其中执行耗时 UI 操作。正确做法是通过 taskpoolsetTimeout 将结果抛回 UI 线程,再更新状态。AudioRenderer 的写入循环建议放在独立 taskpool 任务中,用环形缓冲解耦生产者(解码/合成)与消费者(渲染),这样既能跑满实时性,又不会阻塞主线程交互。

4.9 示例运行前置

运行上述代码前,需在 module.json5 配置 abilitiesskills 与权限,并在 EntryAbility 中持有 UIAbilityContext 传给 init。示例中的 url 可换成官方示例媒体地址,或本地 rawfile 资源。所有 await 调用都应在 async 函数中执行,避免顶层 await 在非 module 场景报错。

4.10 调试技巧

调试播放问题时,优先打印 stateChange 的完整跃迁序列与 error 的错误码;录制问题则检查 fd 是否有效、profile 是否被设备支持。善用 DevEco 的 HiLog 过滤 AVPlayer/AVRecorder 标签,可快速定位是应用层还是引擎层异常。

4.11 音画对齐校验

播放时若发现口型不同步,先确认视频帧时间戳是否连续、是否触发了跳帧。AVPlayer 默认以音频为基准,异常多源于源文件时间戳错乱,可在转码阶段用 ffmpeg 重新生成 PTS 修复。

4.12 录制文件管理

录制结束后,建议把 fd:// 临时文件移动到相册或应用沙箱,并更新 MediaAsset.localPath 与数据库。移动而非复制能避免双份占用;同时生成缩略图,提升列表加载速度。

4.13 多路音频混音

游戏等场景需背景音乐 + 音效多路叠加,可为每路创建独立 AudioRenderer 并设置不同 volume,由系统混音器合成。注意总音量不超过 1.0,防止削波失真;必要时接入 AudioVolumeGroup 管理。

4.14 进度轮询封装

把 AVPlayer 的 currentTime/duration 封装成可观察的进度状态,UI 用 @State@Observed 绑定,避免每个控件各自轮询造成冗余计算与刷新抖动。

4.15 倍速与音调

setSpeedRate 在 0.75x–2x 间平滑切换,UI 提供档位按钮;语音类内容慎用高倍速,以免听感失真影响理解。

4.16 错误上报规范

统一错误上报格式:记录错误码、设备型号、系统版本、媒体格式与发生阶段,便于按机型聚类,快速识别普遍问题还是个别机型兼容问题。

4.17 实例复用池

对频繁切换的短视频场景,可维护一个 AVPlayer 复用池,避免反复 create/release 的开销;池大小按页面并发数设定,退池前必须回到 idle 并清空回调,防止串扰。

4.18 后台播放保活

进入后台前通过 continuousTask 申请长时任务,并把 AVSession 保持激活,系统才允许媒体继续渲染;同时停止预览 Surface 以省电,仅保留音频输出链路。


五、深度技术原理

5.1 媒体引擎流水线

HarmonyOS 媒体能力由下而上分为四层:芯片 VPU/APU 硬件层、Codec HDI 驱动接口层、Media Service 系统服务层、以及应用侧 Kit API 层。AVPlayer 内部流水线为“解封装(Demux)→ 音视频分离 → 解码(Decode)→ 音视频同步 → 渲染(Render)”。所有重计算都发生在系统服务进程,应用侧只持有轻量控制器,这降低了主线程负担与功耗。

5.2 硬解码与软解码路径

硬解路径:AVPlayer 通过 Codec HDI 调用芯片硬解器,输出直接送 GPU 渲染或音频 HAL,CPU 占用极低。软解路径:当硬解器不支持某 Profile(如老设备不支持 HEVC Main10)时,引擎回退到方舟运行时内的软件解码器。开发者可通过 media.AVPlayerdescription 字段读取当前实际解码类型,用于埋点统计。

5.3 同步与渲染

音视频同步采用“以音频时钟为基准(Audio Master)”的经典策略:视频帧按音频播放时间戳对齐,若视频超前则等待、落后则跳帧。渲染阶段,视频走 SurfaceFlinger 合成到 XComponent,音频走 AudioRenderer 写入 AudioService 的混音器,由系统统一调度输出设备。

5.4 媒体会话总线

AVSession 是一个跨应用的“媒体控制总线”。系统维护一个活跃会话栈,仅栈顶会话能接收媒体键事件。应用通过 setAVPlaybackState 上报状态,系统媒体中心(锁屏、控制中心、车机)订阅该状态渲染 UI;用户的按键事件经总线反向派发到对应会话的回调。由此实现了“多应用共存、单焦点控制”的一致体验。

5.5 缓冲、延迟与质量观测

媒体引擎内部维护输入/输出双缓冲队列以平滑抖动:解封装后的包进入解码缓冲,解码帧进入渲染缓冲。播放卡顿通常源于网络抖动导致输入缓冲欠载,此时 AVPlayer 会触发 bufferingUpdate 事件,UI 应展示加载圈并降码率或切换清晰度。

对于实时音频(通话、直播),AudioRenderer 的缓冲深度决定了端到端延迟,过大会增加延迟、过小会爆音。经验值是在 48kHz/20ms 帧长下,缓冲 2–3 帧即可兼顾稳定与低延迟。开发者还可通过 getRendererInfo 观测当前输出设备与采样率,在蓝牙与有线切换时动态调整。

在可观测性上,建议埋点采集“首帧耗时、解码类型(硬/软)、缓冲次数、平均码率”,这些数据能快速定位机型差异化问题,也是后续做自适应码率(ABR)的基础。

5.6 音频焦点与打断管理

当来电、导航播报或闹钟响起时,系统会依据音频焦点策略暂停或压低媒体音量。AVPlayer 会收到 audioInterrupt 回调,携带打断类型(暂停/降低/恢复)。应用应据此暂停播放或降低音量,并在打断结束后(如通话挂断)根据策略自动恢复。合理的焦点处理是上架审核的重点,尤其在车载与穿戴场景。

5.7 渲染管线与显示合成

视频渲染依赖 XComponent 提供的 Surface,媒体引擎把解码后的帧直接送入图形栈,由 SurfaceFlinger 合成到屏幕,绕过应用层拷贝,显著降低延迟与功耗。HDR 内容还会走专用的色彩与亮度管线,需在 XComponent 上声明对应色彩空间。理解这一链路有助于排查“黑屏但有声”“画面撕裂”等典型渲染问题。

5.8 编码与封装格式

媒体引擎对封装格式(MP4、MKV、TS 等)与编码(H.264/HEVC、AAC/Opus)的支持随版本演进。HEVC 在同等画质下体积更小,但老设备硬解支持有限,需做好能力探测。Opus 在低码率语音场景优于 AAC,适合实时通信。选型时应以“目标设备硬解覆盖率”为第一优先级。

5.9 跨设备媒体流转

借助分布式软总线,播放会话的“控制权”可在手机、平板、车机间迁移:源设备把 AVSession 状态与播放位置通过总线同步到目标设备,目标设备接管渲染。底层媒体引擎并不跨设备搬运音视频帧,而是迁移会话 metadata 与进度,由目标设备本地解码,从而保证画质与低延迟。

5.10 低延迟模式

实时乐器、语音聊天对延迟敏感,可开启 AudioRenderer 的低延迟渲染模式,配合更小缓冲与高优先级线程。底层会绕过部分重采样以换取速度,代价是功耗略升,按需启用。

5.11 硬件加速编码

录制侧同样依赖 VPU 编码,鸿蒙按设备能力选择 H.264/HEVC 硬编码器。若需透明通道或特殊参数,可能回退软编,此时应降低分辨率以保帧率。理解这一点有助于在弱机上设定合理上限。

5.12 媒体安全边界

媒体文件解析是安全高发区,应用不应直接 eval 外部描述文件,封面加载走系统沙箱。播放器崩溃应被 Ability 捕获并重启媒体实例,防止被恶意样本打挂整个应用。

5.13 丢帧与卡顿归因

卡顿要先区分是网络缓冲还是解码慢:缓冲类表现为持续转圈且 bufferingUpdate 频繁,解码类表现为掉帧与音画偏移。分别用缓冲埋点与帧率埋点定位,才能对症下药。

5.14 功耗画像

长时间播放建议熄屏后走后台渲染,关闭不必要的日志与预览 Surface,可再降约 10% 整机功耗,对续航敏感的出行与车载场景意义重大。

5.15 画质与码率

同样分辨率下,码率决定清晰度与体积。媒体引擎会按 negotiated 码率分配解码带宽,过高可能触发降频。理解码率-画质-功耗三角,有助于在端侧做出合理取舍。

5.16 缓冲区调优

网络波动大时,可适当增大 AVPlayer 缓冲上限以减少卡顿,但会增加起播延迟;直播场景则相反,需压低缓冲以保实时。缓冲策略应随业务形态(点播/直播)差异化配置。


六、常见问题解答

Q1:设置 url 后直接 play() 为什么报错?
AVPlayer 必须先经历 initializedprepared 状态。正确做法是监听 stateChange,在 initializedprepare(),在 prepared 时再 play(),本文字节已给出标准模板。

Q2:AVRecorder 报 prepare 失败怎么办?
多为 profile 与摄像头能力不匹配(分辨率/帧率/编码不支持)。先用 Camera Kit 查询设备支持的预览与录制能力,再构造一致的 profile。同时确认麦克风、摄像头权限已授予。

Q3:PixelMap 用完要释放吗?
要。createPixelMapcreateImagePacker 都会占用 native 内存,处理完应调用 pixelMap.release()packer.release(),否则在大图批量处理时极易 OOM。

Q4:AudioRenderer 写数据后没声音?
先核对 streamInfo 的采样率、位深、声道与实际 PCM 是否一致;再确认 usageSTREAM_USAGE_MEDIA 且未被系统静音策略拦截;最后检查是否在 start() 之后才写入。

Q5:退到后台媒体键失灵?
需在 onBackground 时不 destroy AVSession,并保持 activate。只有激活态会话才能接收媒体键。另外注意同一时刻系统只把按键派发给栈顶会话。

Q6:如何实现倍速且不变调?
AVPlayer 的 setSpeedRate(rate) 由引擎底层处理,默认变速不变调;若需要变调效果,需自行做重采样或借助音频特效,不在本示例范围内。

Q7:播放网络视频频繁缓冲?
先监听 bufferingUpdate 区分“正在缓冲”与“缓冲结束”,UI 给出加载态;同时确认网络与服务器支持分段(如 HLS/DASH)。若源是单文件大码率,建议转封装为自适应切片,或在弱网下降低清晰度以换取流畅度。

Q8:后台播放被系统杀掉?
确认已申请长时任务(continuousTask)或使用了系统媒体后台模式;同时 AVSession 必须保持 activate。仅持有播放器实例不足以保活,需要配合后台任务声明。

Q9:Image Kit 处理大图 OOM?
大图解码会占用大量 native 内存,建议先用 createImageSourcedesiredSize 做缩略解码,或分块处理;处理完立即 release PixelMap,必要时在 taskpool 中执行以避免阻塞 UI 线程。

Q10:AVSession 元数据不更新?
确认在状态变化后调用了 setAVMetadata/setAVPlaybackState,且会话处于 activate 状态。若切换歌曲,需更新 assetId 以刷新锁屏封面与标题。

Q11:录制视频没有画面只有声音?
通常是 Surface 未正确绑定到 Camera 的录制输出。检查 videoSourceType 是否为 SURFACE_YUV,并在 Camera 配置中把录制 Surface 加入输出列表,再 start 录制。

Q12:切换歌曲时状态错乱?
切歌应先 release 旧 AVPlayer 或复用实例重新 setUrlprepare,同时把 AVSession 的 assetId 与 metadata 一并更新;UI 进度条要 reset 到 0。避免旧回调落到新曲目标题上造成“张冠李戴”。

Q13:列表滚动卡顿?
多为大图未做缩略图,按 3.14 节预生成小图;播放器实例切勿在列表项内创建,应在页面级复用单例,避免频繁初始化带来的卡顿与泄漏。


七、运行效果

播放与录制状态机(ASCII 字符画):

在这里插入图片描述

运行后,控制台依次打印 initialized → prepared → playing,锁屏出现媒体卡片;录制页计时器增长,停止后文件可在播放器回放。

状态机解读:播放器必须严格按 idle→initialized→prepared→playing 推进,任何逆向跳转都需先 release 回到 idle;录制器则允许 prepared 与 recording 间通过 pause/resume 暂挂。两条链路的状态回调都应在 UI 侧做幂等处理,避免重复触发导致界面抖动。

实际设备上,播放页应能看到首帧在 1 秒内出现,拖动进度条 seek 无明显卡顿;锁屏与控制中心实时同步标题与播放/暂停图标;按下耳机中键可正确暂停或恢复。录制页点击开始后计时器递增,停止后回到列表可立即预览。若某机型出现硬解失败,控制台会打印对应 AVError,应按 3.5 节的策略降级处理。

在真机矩阵验证中,旗舰机可稳定硬解 4K,中端机在 1080p 下 CPU 占用约 15%;录制 1080p30 时整机温度上升平缓。锁屏卡片在系统桌面均正常显示,蓝牙耳机双击可切歌。这些指标说明原生媒体栈在性能与体验上已达到生产可用水平。

出现偶发杂音时,优先排查 AudioRenderer 的缓冲深度与采样率是否对齐,其次确认音源本身无损坏;蓝牙设备还要留意协商后的实际输出格式是否与配置一致。


八、扩展方向

  • DRM 版权保护:接入 AVPlayer 的 mediaKey 体系播放加密 HLS/DASH 内容。
  • 画中画(PiP):通过 XComponent + PiP 控制器实现小窗播放。
  • 音频焦点策略:基于 AudioSessionManager 处理来电、导航等打断与混音。
  • 跨端流转:借助分布式软总线把播放会话从手机流转到车机/智慧屏。
  • AI 媒体处理:在录制后接图像分割/语音识别,构建智能剪辑流水线。

落地时可优先实现“播放 + 媒体会话”这一最小闭环,它投入产出比最高:用户无需打开 App 即可在锁屏控制,体验提升明显。录制与图片编辑作为第二阶段,音频渲染则面向有自定义音效需求的专业场景。建议按此优先级排期,并尽早接入真机自动化,把媒体回归纳入日常构建,防止硬解差异在发版前才暴露。

在推进节奏上,建议以双周为迭代:第一迭代打通“播放+会话”,第二迭代补齐“录制+图片”,第三迭代打磨音频渲染与跨端流转。每个迭代都要在真机矩阵上回归,尤其关注芯片差异导致的硬解与录制兼容问题。把媒体能力沉淀为独立 HAR 模块,还能复用到公司内其他应用,形成可复用的媒体中台能力。

此外,媒体能力可进一步封装为团队内部的 HAR 包,对外暴露 MediaKitManager 统一门面,屏蔽底层版本差异与错误码映射。新业务接入只需几行调用即可获得播放、录制、会话接管等能力,大幅缩短交付周期,也让媒体最佳实践在企业内持续沉淀。

最后,建议把媒体回归脚本接入每日构建,覆盖“播放/暂停/seek/录制/编辑/会话接管”主路径,让媒体能力在版本演进中始终有护栏,降低线上故障概率。


Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐