项目为使用uniapp框架开发的Android/iOS APP应用
假设手机正在播放音乐,当前APP处于前台收到消息,需播放提示音提示用户。目标为降低后台正在播放音乐的音量,播放提示音,播放完毕后恢复后台音乐音量
乍一看,需求看似很简单,实则以目前uni官方所封装的API根本无法实现,附上链接uni.createInnerAudioContext,该API中的sessionCategory
配置,实则为当前APP的音频模式,播放的时候影响到其他APP的行为只有一个结果——暂停
很明显,这并非我们能接受的结果,只能另寻他路,既然uni不行,那就只能靠native.js
调用原生方法了
博主找遍了uni官方的文档、论坛,找不到任何相关的功能实现文章。无奈,只能查原生文档,Android、iOS,在阅读了大量原生实现文章后,注意到一个关键词——音频焦点,就是说后续的编码均是围绕着音频焦点而开展的
Android端的实现,需要使用到AudioManager
、MediaPlayer
、AudioAttributes
这三个类,首先AudioManager
实例需要使用Context
去获取
MediaPlayer
类则直接import后new一个新实例就可以了,AudioAttributes
则用来设置所播放音频的属性。最后,还有一个音频播放结束后释放音频焦点的监听方法,这个在Java中为Interface
接口方法——MediaPlayer.OnCompletionListener
参考链接:AudioManager类、MediaPlayer类、AudioAttributes类、
MediaPlayer.OnCompletionListener接口
<script>
export default {
data() {
return {
audioManager: null, // 音频管理
innerAudioContext: {}, // 播放器实例
audioFocus: false // 音频焦点
}
},
created() {
this.buildAudio();
},
methods: {
buildAudio() {
let path = plus.io.convertLocalFileSystemURL('xxx/voice.mp3');
if (this.$store.state.platform == 'android') {
// Android端
// 导入声音管理类(必须步骤,不引会导致功能无法正常运行)
plus.android.importClass('android.media.AudioManager');
let main = plus.android.runtimeMainActivity(); // 获取应用主Activity实例对象
let Context = plus.android.importClass('android.content.Context'); // 全局上下文
this.audioManager = main.getSystemService(Context.AUDIO_SERVICE);
let MediaPlayer = plus.android.importClass('android.media.MediaPlayer');
let AudioAttributes = plus.android.importClass('android.media.AudioAttributes');
this.innerAudioContext = new MediaPlayer();
// Android中的接口实现使用plus.android.implements,注意接口名称类与接口方法之间要用符号$连接
let event = plus.android.implements('android.media.MediaPlayer$OnCompletionListener', {
onCompletion: () => {
if (this.audioFocus) {
this.audioManager.abandonAudioFocus(null); // 放弃音频焦点
this.audioFocus = false;
}
}
});
this.innerAudioContext.setOnCompletionListener(event); // 设置播放完毕监听
this.innerAudioContext.setDataSource(path); // 设置播放器播放音频
this.innerAudioContext.setAudioAttributes(AudioAttributes.CONTENT_TYPE_SONIFICATION); // 设置音频属性,CONTENT_TYPE_SONIFICATION为短暂提示音
this.innerAudioContext.prepare();
} else {
// iOS端...
}
}
}
}
</script>
需要播放音频时,则判断一下后台是否播放着音乐,若正在播放,则请求音频焦点,设置系统类型为音乐类型(STREAM_MUSIC
),设置持续时间提示为短暂打断模式(AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
)
if (this.$store.state.platform === 'android') {
// Android端
if (this.audioManager.isMusicActive()) {
// 判断是否正在播放音乐
this.audioFocus = true;
this.audioManager.requestAudioFocus(null, this.audioManager.STREAM_MUSIC, this.audioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); // 请求音频聚焦
}
this.innerAudioContext.start(); // 播放
} else {
// iOS端...
}
iOS端的实现,需要用到AVAudioSession
、NSData
、AVAudioPlayer
这三个类,其中AVAudioSession
需要使用sharedInstance
获取分享音频会话实例
NSData
用于转换本地文件路径为二进制数据,供AVAudioPlayer
类实例用于初始化播放器。最后,还需要挂载播放完毕的代理方法AVAudioPlayerDelegate
,用于在音频播放结束后设置音频会话激活状态(相当于Android端的释放音频焦点)为false
参考链接:AVAudioSession类、AVAudioPlayer类、NSData类、
AVAudioPlayerDelegate代理
<script>
export default {
methods: {
buildAudio() {
let path = plus.io.convertLocalFileSystemURL('xxx/voice.mp3');
if (this.$store.state.platform == 'android') {
// Android端...
} else {
// iOS端
let AVAudioSession = plus.ios.importClass('AVAudioSession');
this.audioManager = AVAudioSession.sharedInstance(); // 获取分享音频会话实例
/**
* 设置音频会话类型为AVAudioSessionCategoryPlayback,同时配置选项为AVAudioSessionCategoryOptionMixWithOthers和AVAudioSessionCategoryOptionDuckOthers
* Constants that specify optional audio behaviors. setCategory:withOptions:error
* setCategory:(AVAudioSessionCategory)category:
* @param AVAudioSessionCategoryPlayback The category for playing recorded music or other sounds that are central to the successful use of your app.
* withOptions:(AVAudioSessionCategoryOptions)options:
* @param AVAudioSessionCategoryOptionMixWithOthers = 0x1 An option that indicates whether audio from this session mixes with audio from active sessions in other audio apps.
* @param AVAudioSessionCategoryOptionDuckOthers = 0x2 An option that reduces the volume of other audio sessions while audio from this session plays.
*/
this.audioManager.setCategorywithOptionserror('AVAudioSessionCategoryPlayback', 0x1 | 0x2, null);
let NSData = plus.ios.importClass('NSData');
let AVAudioPlayer = plus.ios.importClass('AVAudioPlayer');
let pathFileData = NSData.dataWithContentsOfFile(path); // 音频文件转为二进制数据
this.innerAudioContext = new AVAudioPlayer(); // new一个音频播放器实例
this.innerAudioContext.initWithDataerror(pathFileData, null); // 初始化数据
// iOS中挂载代理方法使用plus.ios.implements,注意挂载的方法名需一个字符不差写完整audioPlayerDidFinishPlaying:successfully:
let delegate = plus.ios.implements('AVAudioPlayerDelegate', {
'audioPlayerDidFinishPlaying:successfully:': () => {
if (this.audioFocus) {
/**
* 设置音频会话激活状态(相当于Android端的音频焦点)为false,同时配置选项为AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation,用于通知其他被中断的APP恢复播放
* Activates or deactivates your app’s audio session using the specified options. setActive:withOptions:error
* setActive:(BOOL)active:
* Specify YES to activate your app’s audio session, or NO to deactivate it.(Objective-C中的YES相当于JS的true,NO相当于false)
* withOptions:(AVAudioSessionSetActiveOptions)options:
* @param AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation = 1 An option that indicates that the system should notify other apps that you’ve deactivated your app’s audio session.
*/
this.audioManager.setActivewithOptionserror(false, 1, null);
this.audioFocus = false;
}
}
});
this.innerAudioContext.plusSetAttribute('delegate', delegate); // 使用plusSetAttribute设置实例对象属性delegate
}
}
}
}
</script>
接下来是音频播放,跟Android端差不多,iOS端这边是激活音频会话
if (this.$store.state.platform === 'android') {
// Android端...
} else {
// iOS端
if (this.audioManager.plusGetAttribute('isOtherAudioPlaying')) {
// 获取判断音频会话属性isOtherAudioPlaying
this.audioFocus = true;
this.audioManager.setActiveerror(true, null); // 激活音频会话
}
this.innerAudioContext.play(); // 播放
}
编码完毕,实际上手调试,在后台其他APP(如音乐播放器)正在播放音乐时,本APP播放提示音,在Android/iOS两端都是表现为降低音乐音量,播放提示音,提示音播放完毕后,恢复音乐音量。
至此,已完美完成需求,这里简单说一下本功能的开发感受:uni官方没有实现的功能,确实通过H5+plus绝大部分功能都是可以实现的,但对于没人写过参考的功能,就只能靠自己查阅原生开发文档、查阅原生开发相关代码,之后尝试使用native.js改写,这整套流程下来,对一个纯前端开发而言确实不容易。但是,博主认为这个过程是收获颇丰的,也是让人有成就感的,毕竟全网第一份。
就说这么多了,Keep learning…