在 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([
- [9230001, '音频播放失败']
- ]);
- 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。 |