【HarmonyOS】使用AVPlayer播放音乐,导致系统其它应用音乐播放暂停 - 播放音频焦点管理
一、前言
在鸿蒙系统中,对于音乐播放分为几种场景。音乐,电影,音效,闹钟等。当使用AVPlayer播放音乐时,如果不处理播放焦点模式,默认会交给系统处理。系统处理多个音乐播放时,会按照触发顺序依次暂停当前,再继续下一个。
例如当华为音乐应用正在播放音乐,此时你的应用使用AVPlayer进行音乐播放,就会导致华为音乐播放暂停,开始播放你的音乐。如果你的是音乐应用,默认这样处理是OK的。但是如果你使用AVPlayer播放一个短时音乐或者音效。那这样处理就不好了。这个问题实际上是播放焦点管理,如果不管理就会造成冲突。
此时我们的预期可以是,播放完短时音乐或者音效后,继续播放华为音乐。或者当我们播放短时音乐或者音效时,声音大。将华为音乐播放声音降低。亦或者是,两个同时播放。
不同的业务场景,需求不同。根据应用产品调性来决定播放处理模式。
二、如何解决播放焦点冲突?
这需要根据你的应用场景来决定。
1.SoundPool 音频池 【完整代码参见章节三】
当你的应用只是播放短时音乐或者音效,那可以不使用AVPlayer,使用SoundPool音频池来处理该场景。效果就是同时播放音乐。你的应用音效播放,并不会干扰其他应用音乐的播放。
只需要设置SoundPool的AudioRendererInfo,配置usage字段为STREAM_USAGE_UNKNOWN,
STREAM_USAGE_MUSIC,
STREAM_USAGE_MOVIE,
STREAM_USAGE_AUDIOBOOK时,为混音模式,不会打断其他音频播放。
let audioRendererInfo: audio.AudioRendererInfo = {usage: audio.StreamUsage.STREAM_USAGE_MUSIC,rendererFlags: 1}let soundPool: media.SoundPool = await media.createSoundPool(5, audioRendererInfo);
SoundPool的使用极其简单,只需要关心音频资源的获取,拿到SoundPool实例后,设置播放的参数,例如播放次数,音量,优先级等。将音频资源赋值后,就可以进行播放。
public async PlaySoundPool() {// 开始播放,这边play也可带播放播放的参数PlayParameters,请在音频资源加载完毕,即收到loadComplete回调之后再执行play操作this.soundPool?.play(this.soundId, this.playParameters, (error, streamID: number) => {if (error) {console.info(this.TAG,`play sound Error: errCode is ${error.code}, errMessage is ${error.message}`)} else {this.streamId = streamID;console.info(this.TAG, 'play success streamID:' + streamID);}});// 设置声道播放音量await this.soundPool?.setVolume(this.streamId, 1, 1);// 设置循环播放次数await this.soundPool?.setLoop(this.streamId, 3); // 播放3次// 设置对应流的优先级await this.soundPool?.setPriority(this.streamId, 1);}
2.播放焦点管理 AudioSessionManager【完整代码参见章节三】
当你的应用是播放音乐时,需要设置音频会话管理的模式,来设置兼容同时播放,还是暂停其他,优先播放当前。亦或者是播放自己的声音大,其他声音小。
如上图可见,AudioSessionManager的创建实际上相当于对播放AVplayer的初始化和播放控制做了一层包裹。包裹之后就可以针对播放进行播放音频焦点的管理。管理具体的播放模式,并发播放,优先播放,声音大播放等。
AudioSessionManager的使用也很简单,只需要拿到实例后,开启会话和关闭会话两个处理即可。开始会话时,需要配置音频焦点模式。
Strategy - concurrencyMode 共有四种模式:
默认模式(CONCURRENCY_DEFAULT):即系统默认的音频焦点策略。
并发模式(CONCURRENCY_MIX_WITH_OTHERS):和其它音频流并发。
降低音量模式(CONCURRENCY_DUCK_OTHERS):和其他音频流并发,并且降低其他音频流的音量。
暂停模式(CONCURRENCY_PAUSE_OTHERS):暂停其他音频流,待释放焦点后通知其他音频流恢复。
private mAudioSessionManager: audio.AudioSessionManager | null = null;private mStrategy: audio.AudioSessionStrategy | null = null;private initAudioSession(){let audioManager = audio.getAudioManager();this.mAudioSessionManager = audioManager.getSessionManager();this.mStrategy = {// 和其它音频流并发。concurrencyMode: audio.AudioConcurrencyMode.CONCURRENCY_MIX_WITH_OTHERS};this.mAudioSessionManager.on('audioSessionDeactivated', (audioSessionDeactivatedEvent: audio.AudioSessionDeactivatedEvent) => {console.info(this.TAG,`reason of audioSessionDeactivated: ${audioSessionDeactivatedEvent.reason} `);});}
在播放音乐前,需要调用开始会话。才能生效音频管理的焦点模式:
如果不做其他处理,在音乐播放完后,一分钟后会话会自动关闭。
// 设置并发播放音频await this.mAudioSessionManager?.activateAudioSession(this.mStrategy);let isActivated = this.mAudioSessionManager?.isAudioSessionActivated();console.log(this.TAG, "play isActivated: " + isActivated);
你也可以在音乐结束后,手动结束会话:
await this.mAudioSessionManager?.deactivateAudioSession();
三、源码示例:
SoundPool 实现短时音乐或者音效播放
SoundPoolMgr.ets
import { audio } from '@kit.AudioKit';
import { media } from '@kit.MediaKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { resourceManager } from '@kit.LocalizationKit';export class SoundPoolMgr {private TAG: string = 'SoundPoolMgr';// 单例对象private static mSoundPoolMgr: SoundPoolMgr | null = null;// 创建单例public static Ins(): SoundPoolMgr{if(!SoundPoolMgr.mSoundPoolMgr){SoundPoolMgr.mSoundPoolMgr = new SoundPoolMgr();}return SoundPoolMgr.mSoundPoolMgr;}private soundPool: media.SoundPool | null = null;private streamId: number = 0;private soundId: number = 0;private playParameters: media.PlayParameters | null = null;public async init(){// audioRenderInfo中的参数usage取值为STREAM_USAGE_UNKNOWN,STREAM_USAGE_MUSIC,STREAM_USAGE_MOVIE,// STREAM_USAGE_AUDIOBOOK时,SoundPool播放短音时为混音模式,不会打断其他音频播放。let audioRendererInfo: audio.AudioRendererInfo = {usage: audio.StreamUsage.STREAM_USAGE_MUSIC,rendererFlags: 1}this.playParameters = {loop: 3, // 循环4次rate: audio.AudioRendererRate.RENDER_RATE_NORMAL, // 正常倍速leftVolume: 1, // range = 0.0-1.0rightVolume: 1, // range = 0.0-1.0priority: 0, // 最低优先级}//创建soundPool实例this.soundPool = await media.createSoundPool(5, audioRendererInfo);let uri: string = "";// // 加载音频资源// await fs.open('/test_01.mp3', fs.OpenMode.READ_ONLY).then((file: fs.File) => {// console.info("file fd: " + file.fd);// uri = 'fd://' + (file.fd).toString()// }); // '/test_01.mp3' 作为样例,使用时需要传入文件对应路径。// this.soundId = await this.soundPool.load(uri);try {let value: resourceManager.RawFileDescriptor = await getContext().resourceManager.getRawFd("test.mp3");let fd = value.fd;let offset = value.offset;let length = value.length;this.soundId = await this.soundPool.load(fd, offset, length);} catch (error) {let code = (error as BusinessError).code;let message = (error as BusinessError).message;console.error(this.TAG,`callback getRawFd failed, error code: ${code}, message: ${message}.`);}// 加载完成回调this.soundPool.on('loadComplete', (soundId_: number) => {console.info(this.TAG, 'loadComplete, soundId: ' + soundId_);})// 播放完成回调this.soundPool.on('playFinished', () => {console.info(this.TAG,"receive play finished message");// 可进行下次播放})//设置错误类型监听this.soundPool.on('error', (error: BusinessError) => {console.info(this.TAG, 'error happened,message is :' + error.message);})}public async PlaySoundPool() {// 开始播放,这边play也可带播放播放的参数PlayParameters,请在音频资源加载完毕,即收到loadComplete回调之后再执行play操作this.soundPool?.play(this.soundId, this.playParameters, (error, streamID: number) => {if (error) {console.info(this.TAG,`play sound Error: errCode is ${error.code}, errMessage is ${error.message}`)} else {this.streamId = streamID;console.info(this.TAG, 'play success streamID:' + streamID);}});// 设置声道播放音量await this.soundPool?.setVolume(this.streamId, 1, 1);// 设置循环播放次数await this.soundPool?.setLoop(this.streamId, 3); // 播放3次// 设置对应流的优先级await this.soundPool?.setPriority(this.streamId, 1);// 设置音量await this.soundPool?.setVolume(this.streamId, 0.5, 0.5);}public async release() {// 终止指定流的播放await this.soundPool?.stop(this.streamId);// 卸载音频资源await this.soundPool?.unload(this.streamId);//关闭监听this.soundPool?.off('loadComplete');this.soundPool?.off('playFinished');this.soundPool?.off('error');// 释放SoundPoolawait this.soundPool?.release();}}
音效播放管理类 添加了AudioSessionManager 播放焦点管理逻辑
AudioMgr .ets
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { audio } from '@kit.AudioKit';/*** 音效播放管理类*/
export class AudioMgr {private TAG: string = 'AudioMgr';// 单例对象private static mAudioMgr: AudioMgr | null = null;// 播放器实例private mAVPlayer: media.AVPlayer | undefined = undefined;// 是否初始化private isInit: boolean = false;private mAudioSessionManager: audio.AudioSessionManager | null = null;private mStrategy: audio.AudioSessionStrategy | null = null;// 创建单例public static Ins(): AudioMgr{if(!AudioMgr.mAudioMgr){AudioMgr.mAudioMgr = new AudioMgr();}return AudioMgr.mAudioMgr;}/*** 初始化接口(可以提前初始化,也可以直接调用play接口,使用时初始化)*/public async init() {console.log(this.TAG, "play init start");let audioManager = audio.getAudioManager();this.mAudioSessionManager = audioManager.getSessionManager();this.mStrategy = {// 和其它音频流并发。concurrencyMode: audio.AudioConcurrencyMode.CONCURRENCY_MIX_WITH_OTHERS};this.mAudioSessionManager.on('audioSessionDeactivated', (audioSessionDeactivatedEvent: audio.AudioSessionDeactivatedEvent) => {console.info(this.TAG,`reason of audioSessionDeactivated: ${audioSessionDeactivatedEvent.reason} `);});// 创建avPlayer实例对象this.mAVPlayer = await media.createAVPlayer();// 创建状态机变化回调函数this.registerStateChange(this.mAVPlayer);// error回调监听函数,当avPlayer在操作过程中出现错误时调用 reset接口触发重置流程this.registerErrorCall(this.mAVPlayer);// 获取raw音效资源let fileDescriptor = await getContext(this).resourceManager.getRawFd("test.mp3");this.mAVPlayer.fdSrc = {fd: fileDescriptor.fd,offset: fileDescriptor.offset,length: fileDescriptor.length};this.isInit = true;console.log(this.TAG, "play init end");return this.mAVPlayer;}/*** 注册异常回调* @param avPlayer*/private registerErrorCall(avPlayer: media.AVPlayer){avPlayer.on('error', (err: BusinessError) => {console.log(this.TAG, " err:" + JSON.stringify(err));// 调用reset重置资源,触发idle状态avPlayer.reset();})}/*** 注册状态变化回调* @param avPlayer*/private registerStateChange(avPlayer: media.AVPlayer){avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {switch (state) {// 成功调用reset接口后触发该状态机上报case 'idle':console.info(this.TAG, 'stateChange idle-release');avPlayer.release(); // 调用release接口销毁实例对象break;// avplayer 设置播放源后触发该状态上报case 'initialized':console.info(this.TAG, 'stateChange initialized-prepare');avPlayer.prepare();break;// prepare调用成功后上报该状态机case 'prepared':console.info(this.TAG, 'stateChange prepared-setVolume');avPlayer.setVolume(1); // The value ranges from 0.00 to 1.00.break;// play成功调用后触发该状态机上报case 'playing':console.info(this.TAG, 'stateChange playing');break;// pause成功调用后触发该状态机上报case 'paused':console.info(this.TAG, 'stateChange paused');break;// 播放结束后触发该状态机上报case 'completed':console.info(this.TAG, 'stateChange completed');break;// stop接口成功调用后触发该状态机上报case 'stopped':console.info(this.TAG, 'stateChange stopped');// avPlayer.reset(); // 调用reset接口初始化avplayer状态break;case 'released':console.info(this.TAG, 'stateChange released');break;default:console.info(this.TAG, 'stateChange default');break;}});}/*** 播放音效*/public async play(){console.log(this.TAG, "play isInit " + this.isInit);// 设置并发播放音频await this.mAudioSessionManager?.activateAudioSession(this.mStrategy);let isActivated = this.mAudioSessionManager?.isAudioSessionActivated();console.log(this.TAG, "play isActivated: " + isActivated);if(this.isInit){await this.mAVPlayer?.play();}else{console.log(this.TAG, "play play-init start");this.mAVPlayer = await this.init();console.log(this.TAG, "play play start");await this.mAVPlayer?.play();console.log(this.TAG, "play play end");}}/*** 销毁音效管理工具*/public async destroy(){console.log(this.TAG, "play destroy start");await this.mAVPlayer?.release();this.mAVPlayer = undefined;this.isInit = false;AudioMgr.mAudioMgr = null;console.log(this.TAG, "play destroy end");await this.mAudioSessionManager?.deactivateAudioSession();}}
注意
当应用通过AudioSession使用上述各种模式时,系统将尽量满足其焦点策略,但在所有场景下可能无法保证完全满足。
如使用CONCURRENCY_PAUSE_OTHERS模式时,Movie流申请音频焦点,如果Music流正在播放,则Music流会被暂停。但是如果VoiceCommunication流正在播放,则VoiceCommunication流不会被暂停。