基于AVPlayer音频后台播放
场景描述
音乐播放是媒体最重要的组成之一,以下是AVPlayer将Audio媒体资源(比如mp3等)转码为可听见的音频模拟信号,并通过输出设备进行播放。
场景一:使用avPlayer进行后台播放音乐
想要实现应用后台播放,那么接入AVSession是必须的,否则业务的正常功能会同时受到限制,也必须有BackgroundTasks Kit(后台任务管理)的能力,申请对应的长时任务,避免进入挂起(Suspend)状态。
应用不申请后台任务会被冻结,不注册AVSession会被暂停。
步骤一:创建avPlayer实现音频播放
创建avPlayer并加载音频资源
async avPlayerFdSrcDemo() {
// 创建avPlayer实例对象
avPlayer = await media.createAVPlayer();
// 创建状态机变化回调函数
this.setAVPlayerCallback(avPlayer);
// 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址
let context = getContext(this) as common.UIAbilityContext;
let fileDescriptor = await context.resourceManager.getRawFd('123.mp3');
// 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度
let avFileDescriptor: media.AVFileDescriptor =
{ fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length };
this.isSeek = true; // 支持seek操作
// 为fdSrc赋值触发initialized状态机上报
avPlayer.fdSrc = avFileDescriptor;
}
注册avPlayer回调函数
// 注册avplayer回调函数
setAVPlayerCallback(avPlayer: media.AVPlayer) {
// seek操作结果回调函数
avPlayer.on('seekDone', (seekDoneTime: number) => {
console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
})
// error回调监听函数,当avPlayer在操作过程中出现错误时调用 reset接口触发重置流程
avPlayer.on('error', (err: BusinessError) => {
console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
avPlayer.reset(); // 调用reset重置资源,触发idle状态
})
// 状态机变化回调函数
avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
switch (state) {
case 'idle': // 成功调用reset接口后触发该状态机上报
console.info('AVPlayer state idle called.');
avPlayer.release(); // 调用release接口销毁实例对象
break;
case 'initialized': // avplayer 设置播放源后触发该状态上报
console.info('AVPlayer state initialized called.');
avPlayer.prepare();
break;
case 'prepared': // prepare调用成功后上报该状态机
console.info('AVPlayer state prepared called.');
avPlayer.audioInterruptMode=audio.InterruptMode.SHARE_MODE;
avPlayer.play(); // 调用播放接口开始播放
break;
case 'playing': // play成功调用后触发该状态机上报
console.info('AVPlayer state playing called.');
break;
case 'paused': // pause成功调用后触发该状态机上报
console.info('AVPlayer state paused called.');
avPlayer.play(); // 再次播放接口开始播放
break;
case 'completed': // 播放结束后触发该状态机上报
console.info('AVPlayer state completed called.');
avPlayer.stop(); //调用播放结束接口
break;
case 'stopped': // stop接口成功调用后触发该状态机上报
console.info('AVPlayer state stopped called.');
avPlayer.reset(); // 调用reset接口初始化avplayer状态
break;
case 'released':
console.info('AVPlayer state released called.');
break;
default:
console.info('AVPlayer state unknown called.');
break;
}
})
}
步骤二:创建AVSession,使音频接入播控中心
AVSession在构造方法中支持不同的类型参数,由 AVSessionType定义,不同的类型代表了不同场景的控制能力,对于播控中心来说,会展示不同的控制模版。
- audio类型,播控中心的控制样式为:收藏,上一首,播放/暂停,下一首,循环模式。
- video类型,播控中心的控制样式为:快退,上一首,播放/暂停,下一首,快进。
- voice_call类型,通话类型。
创建AVSession
async createSession() {
let type: AVSessionManager.AVSessionType = 'audio';
/*
* context:应用上下文,提供获取应用程序环境信息的能力。
* tag:会话的自定义名称。
*type:会话类型。
*/
let session = await AVSessionManager.createAVSession(context,'SESSION_NAME', type);
// 设置必要的媒体信息
let metadata: AVSessionManager.AVMetadata = {
assetId: '0', // 由应用指定,用于标识应用媒体库里的媒体
title: 'TITLE',
mediaImage: 'IMAGE',
artist: 'ARTIST',
};
session.setAVMetadata(metadata).then(() => {
console.info(`SetAVMetadata successfully`);
}).catch((err: BusinessError) => {
console.error(`Failed to set AVMetadata. Code: ${err.code}, message: ${err.message}`);
});
//监听事件
this.setListenerForMesFromController(session)
// 激活接口要在元数据、控制命令注册完成之后再执行
await session.activate();
console.info(`session create done : sessionId : ${session.sessionId}`);
}
注:播控中心的显示必须要配上session.on控制命令的监听
async setListenerForMesFromController(session: avSession.AVSession) {
// 一般在监听器中会对播放器做相应逻辑处理
// 不要忘记处理完后需要通过set接口同步播放相关信息,参考上面的用例
session.on('play', () => {
console.info(`on play , do play task`);
// 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('play')取消监听
// 处理完毕后,请使用SetAVPlayState上报播放状态
});
session.on('pause', () => {
console.info(`on pause , do pause task`);
// 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('pause')取消监听
// 处理完毕后,请使用SetAVPlayState上报播放状态
});
session.on('stop', () => {
console.info(`on stop , do stop task`);
// 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('stop')取消监听
// 处理完毕后,请使用SetAVPlayState上报播放状态
});
session.on('playNext', () => {
console.info(`on playNext , do playNext task`);
// 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('playNext')取消监听
// 处理完毕后,请使用SetAVPlayState上报播放状态,使用SetAVMetadata上报媒体信息
});
session.on('playPrevious', () => {
console.info(`on playPrevious , do playPrevious task`);
// 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('playPrevious')取消监听
// 处理完毕后,请使用SetAVPlayState上报播放状态,使用SetAVMetadata上报媒体信息
});
session.on('fastForward', () => {
console.info(`on fastForward , do fastForward task`);
// 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('fastForward')取消监听
// 处理完毕后,请使用SetAVPlayState上报播放状态和播放position
});
session.on('rewind', () => {
console.info(`on rewind , do rewind task`);
// 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('rewind')取消监听
// 处理完毕后,请使用SetAVPlayState上报播放状态和播放position
});
session.on('seek', (time) => {
console.info(`on seek , the seek time is ${time}`);
// 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('seek')取消监听
// 处理完毕后,请使用SetAVPlayState上报播放状态和播放position
});
session.on('setSpeed', (speed) => {
console.info(`on setSpeed , the speed is ${speed}`);
// do some tasks ···
});
session.on('setLoopMode', (mode) => {
console.info(`on setLoopMode , the loop mode is ${mode}`);
// 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('setLoopMode')取消监听
// 应用自定下一个模式,处理完毕后,请使用SetAVPlayState上报切换后的LoopMode
});
session.on('toggleFavorite', (assetId) => {
console.info(`on toggleFavorite , the target asset Id is ${assetId}`);
// 如暂不支持该指令,请勿注册;或在注册后但暂不使用时,通过session.off('toggleFavorite')取消监听
// 处理完毕后,请使用SetAVPlayState上报收藏结果isFavorite
});
}
步骤三:创建长时任务
在module.json5申请ohos.permission.KEEP_BACKGROUND_RUNNING权限:
"requestPermissions": [
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
"reason": "$string:app_name",
"usedScene": {
"abilities": [
"FormAbility"
],
"when":"always"
}
},
]
声明后台模式类型
在对应的UIAbility下配置backgroundModes
"backgroundModes": [
// 长时任务类型的配置项
"audioPlayback"
]
配置长时任务信息
let wantAgentInfo: wantAgent.WantAgentInfo = {
// 点击通知后,将要执行的动作列表
// 添加需要被拉起应用的bundleName和abilityName
wants: [
{
bundleName: "com.example.avplayerdemo",
abilityName: "com.example.avplayerdemo.EntryAbility"
}
],
// 指定点击通知栏消息后的动作是拉起ability
actionType: wantAgent.OperationType.START_ABILITY,
// 使用者自定义的一个私有值
requestCode: 0,
// 点击通知后,动作执行属性
wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
};
wantAgentInfo配置信息链接:WantAgentInfo。
申请长时任务
// 通过wantAgent模块下getWantAgent方法获取WantAgent对象
wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj: WantAgent) => {
backgroundTaskManager.startBackgroundRunning(context,
backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, wantAgentObj).then(() => {
console.info(`Succeeded in operationing startBackgroundRunning.`);
}).catch((err: BusinessError) => {
console.error(`Failed to operation startBackgroundRunning. Code is ${err.code}, message is ${err.message}`);
});
});
场景二:在播放音乐过程中,有其它音频流(如:导航音、电话)进入,进行相关处理
在多个音频流同时播放场景下,如果系统不加管控,会造成多个音频流混音播放,容易让用户感到嘈杂,造成不好的用户体验。为了解决这个问题,系统预设了音频打断(InterruptEvent)策略,对多音频播放的并发进行管控。为满足应用对多音频并发策略的不同需求,音频打断策略预设了两种焦点模式,针对同一应用创建的多个音频流,应用可通过设置焦点模式,选择由应用自主管控或由系统统一管控。
步骤一:完成上述场景一。
步骤二:设置焦点模式。
- 共享焦点模式(SHARE_MODE):由同一应用创建的多个音频流,共享一个音频焦点。这些音频流之间的并发规则由应用自主决定,音频打断策略不会介入。当其他应用创建的音频流与该应用的音频流并发播放时,才会触发音频打断策略的管控。
- 独立焦点模式(INDEPENDENT_MODE):应用创建的每一个音频流均会独立拥有一个音频焦点,当多个音频流并发播放时,会触发音频打断策略的管控。
avPlayer.audioInterruptMode=audio.InterruptMode.SHARE_MODE;
注:只允许在prepared/playing/paused/completed状态下设置。
步骤三:设置音频类型。
let audioRendererInfo: audio.AudioRendererInfo = {
usage: audio.StreamUsage.STREAM_USAGE_NAVIGATION, // 音频流使用类型
rendererFlags: 0 // 音频渲染器标志,0代表普通音频渲染器,1代表低时延音频渲染器。ArkTS接口暂不支持低时延音频渲染器。
}
avPlayer.audioRendererInfo=audioRendererInfo;
步骤四:创建监听音频焦点打断。
调用avPlayer的on('audioInterrupt')函数进行监听,当收到音频打断事件(InterruptEvent)时,应用需根据其内容,做出相应的调整。
avPlayer.on('audioInterrupt', async(interruptEvent: audio.InterruptEvent) => {
// 先读取interruptEvent.forceType的类型,判断系统是否已强制执行相应操作
// 再读取interruptEvent.hintType的类型,做出相应的处理
if (interruptEvent.forceType === audio.InterruptForceType.INTERRUPT_FORCE) {
// 强制打断类型(INTERRUPT_FORCE):音频相关处理已由系统执行,应用需更新自身状态,做相应调整
switch (interruptEvent.hintType) {
case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
// 此分支表示系统已将音频流暂停(临时失去焦点),为保持状态一致,应用需切换至音频暂停状态
// 临时失去焦点:待其他音频流释放音频焦点后,本音频流会收到resume对应的音频打断事件,到时可自行继续播放
isPlay = false; // 此句为简化处理,代表应用切换至音频暂停状态的若干操作
break;
case audio.InterruptHint.INTERRUPT_HINT_STOP:
// 此分支表示系统已将音频流停止(永久失去焦点),为保持状态一致,应用需切换至音频暂停状态
// 永久失去焦点:后续不会再收到任何音频打断事件,若想恢复播放,需要用户主动触发。
isPlay = false; // 此句为简化处理,代表应用切换至音频暂停状态的若干操作
break;
case audio.InterruptHint.INTERRUPT_HINT_DUCK:
// 此分支表示系统已将音频音量降低(默认降到正常音量的20%),为保持状态一致,应用需切换至降低音量播放状态
// 若应用不接受降低音量播放,可在此处选择其他处理方式,如主动暂停等
isDucked = true; // 此句为简化处理,代表应用切换至降低音量播放状态的若干操作
break;
case audio.InterruptHint.INTERRUPT_HINT_UNDUCK:
// 此分支表示系统已将音频音量恢复正常,为保持状态一致,应用需切换至正常音量播放状态
isDucked = false; // 此句为简化处理,代表应用切换至正常音量播放状态的若干操作
break;
default:
break;
}
}
});
场景:
新播放的音频流 | ||||||||
voip通话 | voip消息 | 音乐 | 视频 | 游戏 | 听书/听新闻 | 导航 | ||
正在播放的音频流 | voip通话 | 拒绝新的voip通话 | 降低voip消息音量 | 降低音乐音量 | 降低视频音量 | 降低游戏音量 | 降低听书/听新闻音量 | 降低导航音量 |
voip消息 | 停止正在播放的voip消息 | 停止正在播放的voip消息 | 停止voip消息 | 停止voip消息 | 同时播放 | 停止voip消息 | 降低导航音量 | |
音乐 | 暂停音乐 | 音乐 | 暂停音乐 | 停止音乐 | 停止正在播放的音乐 | 停止音乐 | 同时播放 | |
视频 | 暂停视频 | 视频 | 暂停视频 | 停止视频 | 停止视频 | 停止正在播放的视频 | 停止视频 | |
游戏 | 暂停游戏 | 游戏 | 暂停游戏 | 同时播放 | 同时播放 | 暂停游戏 | 停止正在播放的游戏 | |
听书/听新闻 | 暂停听书/听新闻 | 听书/听新闻 | 暂停听书/听新闻 | 停止听书/听新闻 | 停止听书/听新闻 | 停止听书/听新闻 | 停止听书/听新闻 | |
导航 | 降低导航音量 | 导航 | 降低导航音量 | 降低导航音量 | 降低音乐音量 | 降低视频音量 | 降低游戏音量 |