【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时,为混音模式,不会打断其他音频播放。
dart
let audioRendererInfo: audio.AudioRendererInfo = {
usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
rendererFlags: 1
}
let soundPool: media.SoundPool = await media.createSoundPool(5, audioRendererInfo);
SoundPool的使用极其简单,只需要关心音频资源的获取,拿到SoundPool实例后,设置播放的参数,例如播放次数,音量,优先级等。将音频资源赋值后,就可以进行播放。
dart
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):暂停其他音频流,待释放焦点后通知其他音频流恢复。
dart
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} `);
});
}
在播放音乐前,需要调用开始会话。才能生效音频管理的焦点模式:
如果不做其他处理,在音乐播放完后,一分钟后会话会自动关闭。
dart
// 设置并发播放音频
await this.mAudioSessionManager?.activateAudioSession(this.mStrategy);
let isActivated = this.mAudioSessionManager?.isAudioSessionActivated();
console.log(this.TAG, "play isActivated: " + isActivated);
你也可以在音乐结束后,手动结束会话:
dart
await this.mAudioSessionManager?.deactivateAudioSession();
三、源码示例:
SoundPool 实现短时音乐或者音效播放
SoundPoolMgr.ets
dart
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.0
rightVolume: 1, // range = 0.0-1.0
priority: 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');
// 释放SoundPool
await this.soundPool?.release();
}
}
音效播放管理类 添加了AudioSessionManager 播放焦点管理逻辑
AudioMgr .ets
dart
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流不会被暂停。