uniapp实现音频播放抢占系统音频焦点

项目为使用uniapp框架开发的Android/iOS APP应用

实现功能需求

假设手机正在播放音乐,当前APP处于前台收到消息,需播放提示音提示用户。目标为降低后台正在播放音乐的音量,播放提示音,播放完毕后恢复后台音乐音量

需求分析

乍一看,需求看似很简单,实则以目前uni官方所封装的API根本无法实现,附上链接uni.createInnerAudioContext,该API中的sessionCategory配置,实则为当前APP的音频模式,播放的时候影响到其他APP的行为只有一个结果——暂停
很明显,这并非我们能接受的结果,只能另寻他路,既然uni不行,那就只能靠native.js调用原生方法了

查阅文档

博主找遍了uni官方的文档、论坛,找不到任何相关的功能实现文章。无奈,只能查原生文档,AndroidiOS,在阅读了大量原生实现文章后,注意到一个关键词——音频焦点,就是说后续的编码均是围绕着音频焦点而开展的

Android端

Android端的实现,需要使用到AudioManagerMediaPlayerAudioAttributes这三个类,首先AudioManager实例需要使用Context去获取
AudioManager类
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
requestAudioFocus

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端

iOS端的实现,需要用到AVAudioSessionNSDataAVAudioPlayer这三个类,其中AVAudioSession需要使用sharedInstance获取分享音频会话实例
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…

猜你喜欢

转载自blog.csdn.net/weixin_43905402/article/details/128484353