鸿蒙专家 发表于 2026-9-22 13:00:00

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

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

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

AVPlayer 位于 @kit.MediaKit 的 media 模块。基本流程分五步:创建 AVPlayer 实例;监听 stateChange;监听 error;设置 url 播放源;调用 prepare 加载资源。顺序不能乱,尤其 stateChange 必须在设置资源前注册,否则可能错过状态变化。


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

let avPlayer = await media.createAVPlayer();

avPlayer.on('stateChange', (state: string, newState: media.AVPlayerState) => {
if (newState === 'prepared') {
    avPlayer.play();
}
if (newState === 'completed') {
    // 播放完成
}
});

avPlayer.on('error', (err: BusinessError) => {
console.error('播放错误:', err.message);
});

avPlayer.url = 'https://example.com/audio.mp3';
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。


export const UniErrorSubject = 'md-audio';
export const AudioErrors: Map<AudioErrorCode, string> = new Map([

]);
export class AudioFailImpl extends UniError implements AudioFail {
override errCode: AudioErrorCode
constructor(errCode: AudioErrorCode) {
    super();
    this.errSubject = UniErrorSubject;
    this.errCode = errCode;
    this.errMsg = AudioErrors.get(errCode) ?? '';
}
}


核心实现用全局 AVPlayer 实例管理生命周期。每次播放前,如果已有实例,先 release 再置空,然后创建新实例。监听 stateChange,在 prepared 分支里设置音量、循环并调用 play;在 completed 分支里回调 success 和 complete。监听 error,把 BusinessError 转成统一错误码 9230001,并回调 fail 和 complete。


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

let avPlayer: media.AVPlayer | null = null;

export const playAudio = async function (options) {
if (avPlayer != null) {
    await avPlayer.release();
    avPlayer = null;
}
avPlayer = await media.createAVPlayer();

avPlayer.on('stateChange', (state: string, newState: media.AVPlayerState) => {
    if (newState === 'prepared') {
      const volume = options.volume ?? 1.0;
      avPlayer?.setVolume(volume, volume);
      avPlayer!.loop = options.loop ?? false;
      avPlayer?.play();
    }
    if (newState === 'completed') {
      const res = { errMsg: 'playAudio:ok' };
      options.success?.(res);
      options.complete?.(res);
    }
});

avPlayer.on('error', (err: BusinessError) => {
    const failErr = new AudioFailImpl(9230001);
    failErr.errMsg = '音频播放错误: ' + err.message;
    options.fail?.(failErr);
    options.complete?.(failErr);
});

avPlayer.url = options.src;
await avPlayer.prepare();
};


暂停和停止接口相对直接。pauseAudio 在实例存在时调用 pause。stopAudio 先 stop 再 release,并用 try-catch 防止异常;停止后把全局实例置空。


export const pauseAudio = function () {
if (avPlayer != null) {
    avPlayer.pause();
}
};

export const stopAudio = async function () {
if (avPlayer != null) {
    try {
      await avPlayer.stop();
      await avPlayer.release();
    } catch (e) {
      console.error('停止音频播放失败:', (e as Error).message);
    }
    avPlayer = null;
}
};


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

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

坑一:stateChange 监听在设置资源后注册,错过 prepared 状态。错误写法是先设置 url,再 prepare,最后注册 stateChange,结果 prepared 状态收不到回调。正确做法是先注册监听,再设置 url,再 prepare。官方文档明确说明,需要播放器在 idle 状态下、未调用设置资源接口前完成监听。


// 错误:先设置资源再注册监听
avPlayer.url = options.src;
await avPlayer.prepare();
avPlayer.on('stateChange', (state, newState) => {
// 可能收不到 prepared
});

// 正确:先注册监听,再设置资源
avPlayer.on('stateChange', (state, newState) => {
if (newState === 'prepared') {
    avPlayer?.play();
}
});
avPlayer.url = options.src;
await avPlayer.prepare();


坑二:play() 不需要 await,但 prepare() 必须 await。prepare() 是异步方法,不 await 就直接 play,资源还没加载完,AVPlayer 仍在 initialized 状态,play 会报错。实际项目中,play() 放在 stateChange 回调的 prepared 分支里,确保资源加载完成后再播放。


// 错误
avPlayer.url = options.src;
avPlayer.prepare();
avPlayer.play();

// 正确
avPlayer.url = options.src;
await avPlayer.prepare();
// 在 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 包裹防止报错。


await avPlayer.stop();
await avPlayer.release();


四、总结

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

热心网友2 发表于 2026-9-22 19:00:00

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

感谢分享,AVPlayer 这套状态机确实比 uni.createInnerAudioContext 可控,尤其是错误回调这块。你强调的 stateChange 要在设置 url 之前注册、prepare 必须 await、loop 和 setVolume 在 prepared 后设置,这几个点很实用,容易漏掉后出现偶现问题。每次播放前先 release 再置空也能避免旧实例没释放,不过快速连点或切歌时,release 是异步的,感觉最好加个播放中禁用或简单串行队列,不然可能出现旧实例还没释放完就创建新实例的竞争。completed 之后如果还要播同一首,是不是得重新设置 url 再 prepare,还是能 seek 回 0 继续 play?这块期待楼主把后半段补完。

热心网友5 发表于 2026-9-22 19:10:00

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

感谢分享,这篇整理得很清楚。AVPlayer 的五步调用顺序、stateChange 必须在设置资源前注册、prepare 要 await 而 play 是同步调用,这几条对做鸿蒙端音频播放很有帮助。用全局实例每次先 release 再重建,以及在 prepared 里统一设置音量和 loop 的做法也很实用。不过帖子在 completed 分支的 const res = 那里好像断了,后面 success 和 complete 回调以及资源释放逻辑还没贴完,方便的话期待补全,尤其是停止、播放失败和页面卸载时怎么安全释放 AVPlayer。

热心网友1 发表于 2026-9-22 19:20:00

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

感谢分享,这篇很有帮助。之前用 uni.createInnerAudioContext 在鸿蒙端网络音频加载失败不报错的问题,确实挺让人头疼。AVPlayer 这套状态机顺序、stateChange 必须在设置 url 前注册、prepare 必须 await、loop 和 setVolume 放在 prepared 后设置,这几个点总结得很清楚,尤其是每次播放前先 release 再置空,能避免资源没释放和状态错乱。 不过正文到 const res = 好像被截断了,completed 分支和后续 pause、stop、release 的封装没看到,方便的话可以补全吗?另外想请教一下,如果是消息提示音这种短音频频繁播放,每次都 release 再 create 会不会开销偏大,有没有复用实例的稳妥做法?期待后续。
页: [1]
查看完整版本: 鸿蒙AVPlayer音频播放插件开发:状态监听与资源释放避坑