查看: 209|回复: 0

鸿蒙AVPlayer音频播放插件开发:状态监听与资源释放避坑

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
在 uni-app x 项目里做音乐播放器、语音播报或消息提示音时,音频播放是绕不开的基础能力。最初尝试 uni.createInnerAudioContext,但在鸿蒙端行为不太一致,网络音频加载失败时甚至不报错。项目需要播放网络音频,并支持暂停、停止、循环播放和音量调节。最终选择鸿蒙 @kit.MediaKit 的 AVPlayer:它是官方推荐的音视频播放器,AudioPlayer 从 API 9 开始已经废弃,AVPlayer 的状态机清晰、错误回调完善,比 uni 的封装更可控。

一、AVPlayer 的调用顺序与状态机

AVPlayer 位于 @kit.MediaKit 的 media 模块。基本流程分五步:创建 AVPlayer 实例;监听 stateChange;监听 error;设置 url 播放源;调用 prepare 加载资源。顺序不能乱,尤其 stateChange 必须在设置资源前注册,否则可能错过状态变化。
  1. import { media } from '@kit.MediaKit';
  2. import { BusinessError } from '@kit.BasicServicesKit';
  3. let avPlayer = await media.createAVPlayer();
  4. avPlayer.on('stateChange', (state: string, newState: media.AVPlayerState) => {
  5.   if (newState === 'prepared') {
  6.     avPlayer.play();
  7.   }
  8.   if (newState === 'completed') {
  9.     // 播放完成
  10.   }
  11. });
  12. avPlayer.on('error', (err: BusinessError) => {
  13.   console.error('播放错误:', err.message);
  14. });
  15. avPlayer.url = 'https://example.com/audio.mp3';
  16. await avPlayer.prepare();
复制代码

url 属性支持 http/https 网络播放、HLS 网络播放,也支持 fd:// 文件描述符播放。支持的音频格式包括 m4a、aac、mp3、ogg、wav、flac、amr、ape。网络播放需要在调用方声明 ohos.permission.INTERNET 权限。

AVPlayerState 的关键状态有:idle 初始状态;initialized 设置资源后;prepared prepare 调用成功、可以播放;playing 播放中;paused 暂停中;completed 播放完成;stopped 停止后;released 释放后。loop 属性控制循环播放,true 表示循环,设置时机在 prepared 状态后、play 前。setVolume(leftVolume, rightVolume) 设置左右声道音量,范围 0.0-1.0,同样在 prepared 状态后设置。prepare() 是异步方法,调用后从 initialized 转为 prepared,网络音频需要等待下载元数据,本地音频几乎瞬间完成。play() 不需要 await,它是同步方法,但 prepare() 必须 await,否则资源没加载完就播放会报错。

二、插件结构与核心实现

插件代码放在 uni_modules/md-audio 目录下,示例页面在 pages/audio/audio.uvue。接口层定义 AudioResult、AudioOptions,以及 playAudio、pauseAudio、stopAudio 函数签名。错误码使用 9230001,并通过 AudioFailImpl 统一封装 UniError。
  1. export const UniErrorSubject = 'md-audio';
  2. export const AudioErrors: Map<AudioErrorCode, string> = new Map([
  3.   [9230001, '音频播放失败']
  4. ]);
  5. export class AudioFailImpl extends UniError implements AudioFail {
  6.   override errCode: AudioErrorCode
  7.   constructor(errCode: AudioErrorCode) {
  8.     super();
  9.     this.errSubject = UniErrorSubject;
  10.     this.errCode = errCode;
  11.     this.errMsg = AudioErrors.get(errCode) ?? '';
  12.   }
  13. }
复制代码

核心实现用全局 AVPlayer 实例管理生命周期。每次播放前,如果已有实例,先 release 再置空,然后创建新实例。监听 stateChange,在 prepared 分支里设置音量、循环并调用 play;在 completed 分支里回调 success 和 complete。监听 error,把 BusinessError 转成统一错误码 9230001,并回调 fail 和 complete。
  1. import { media } from '@kit.MediaKit';
  2. import { BusinessError } from '@kit.BasicServicesKit';
  3. let avPlayer: media.AVPlayer | null = null;
  4. export const playAudio = async function (options) {
  5.   if (avPlayer != null) {
  6.     await avPlayer.release();
  7.     avPlayer = null;
  8.   }
  9.   avPlayer = await media.createAVPlayer();
  10.   avPlayer.on('stateChange', (state: string, newState: media.AVPlayerState) => {
  11.     if (newState === 'prepared') {
  12.       const volume = options.volume ?? 1.0;
  13.       avPlayer?.setVolume(volume, volume);
  14.       avPlayer!.loop = options.loop ?? false;
  15.       avPlayer?.play();
  16.     }
  17.     if (newState === 'completed') {
  18.       const res = { errMsg: 'playAudio:ok' };
  19.       options.success?.(res);
  20.       options.complete?.(res);
  21.     }
  22.   });
  23.   avPlayer.on('error', (err: BusinessError) => {
  24.     const failErr = new AudioFailImpl(9230001);
  25.     failErr.errMsg = '音频播放错误: ' + err.message;
  26.     options.fail?.(failErr);
  27.     options.complete?.(failErr);
  28.   });
  29.   avPlayer.url = options.src;
  30.   await avPlayer.prepare();
  31. };
复制代码

暂停和停止接口相对直接。pauseAudio 在实例存在时调用 pause。stopAudio 先 stop 再 release,并用 try-catch 防止异常;停止后把全局实例置空。
  1. export const pauseAudio = function () {
  2.   if (avPlayer != null) {
  3.     avPlayer.pause();
  4.   }
  5. };
  6. export const stopAudio = async function () {
  7.   if (avPlayer != null) {
  8.     try {
  9.       await avPlayer.stop();
  10.       await avPlayer.release();
  11.     } catch (e) {
  12.       console.error('停止音频播放失败:', (e as Error).message);
  13.     }
  14.     avPlayer = null;
  15.   }
  16. };
复制代码

示例页面提供音频 URL 输入、循环播放开关、音量滑块,以及播放、暂停、停止按钮。播放时调用 playAudio,传入 src、loop、volume、success、fail;暂停调用 pauseAudio;停止调用 stopAudio。

三、五个常见问题与处理方式

坑一:stateChange 监听在设置资源后注册,错过 prepared 状态。错误写法是先设置 url,再 prepare,最后注册 stateChange,结果 prepared 状态收不到回调。正确做法是先注册监听,再设置 url,再 prepare。官方文档明确说明,需要播放器在 idle 状态下、未调用设置资源接口前完成监听。
  1. // 错误:先设置资源再注册监听
  2. avPlayer.url = options.src;
  3. await avPlayer.prepare();
  4. avPlayer.on('stateChange', (state, newState) => {
  5.   // 可能收不到 prepared
  6. });
  7. // 正确:先注册监听,再设置资源
  8. avPlayer.on('stateChange', (state, newState) => {
  9.   if (newState === 'prepared') {
  10.     avPlayer?.play();
  11.   }
  12. });
  13. avPlayer.url = options.src;
  14. await avPlayer.prepare();
复制代码

坑二:play() 不需要 await,但 prepare() 必须 await。prepare() 是异步方法,不 await 就直接 play,资源还没加载完,AVPlayer 仍在 initialized 状态,play 会报错。实际项目中,play() 放在 stateChange 回调的 prepared 分支里,确保资源加载完成后再播放。
  1. // 错误
  2. avPlayer.url = options.src;
  3. avPlayer.prepare();
  4. avPlayer.play();
  5. // 正确
  6. avPlayer.url = options.src;
  7. await avPlayer.prepare();
  8. // 在 stateChange 的 prepared 分支里 play
复制代码

坑三:网络播放忘记声明 INTERNET 权限。播放网络音频时,如果 module.json5 的 requestPermissions 中没有 ohos.permission.INTERNET,url 设置后直接走 error 回调,错误信息是 Permission verification failed。插件本身不处理权限声明,调用方需要自己添加。如果是本地文件路径,例如 fd:// 或 /data/storage/,不需要网络权限。

坑四:多次播放没有释放旧实例。连续点击播放按钮,每次都会创建新的 AVPlayer 实例,旧实例没有释放,内存越占越多。处理方式是用全局变量保存 AVPlayer 实例,每次播放前检查是否有旧实例,有就先 release,再创建新实例。release 后实例才能被垃圾回收。

坑五:stop 后直接 release 报错。调用 stop() 后 AVPlayer 进入 stopped 状态,这时可以直接 release()。但如果在 playing 状态直接 release(),会报错。正确顺序是先 stop 再 release。插件的 stopAudio 方法里,先调用 stop(),再调用 release(),并用 try-catch 包裹防止报错。
  1. await avPlayer.stop();
  2. await avPlayer.release();
复制代码

四、总结

音频播放插件的核心是 AVPlayer 状态机管理。难点在于理解状态转换顺序、正确处理异步操作,以及及时释放资源避免内存泄漏。把 AVPlayer 生命周期收敛到 UTS 插件内部,业务侧只需要传入 src、loop、volume,并处理统一错误码 9230001,可以减少调用方的状态管理成本。插件代码位于 uni_modules/md-audio,示例页面位于 pages/audio/audio.uvue。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-22 15:22 , Processed in 0.033319 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部