承接上回语音功能的实现《uniapp实现语音播放功能》,仍存在有瑕疵,问题如下:
1. 语音播放时,会直接打断后台正在播放的音乐,播放完毕无自动恢复音乐
2. 语音播放的输出(耳机/扬声器)全凭各种品牌手机的实现,不可控
基于此,博主又深入研究了一下,目前实现了Android端
的优化方案,暂无iOS端 o(╥﹏╥)o
背景
首先,语音播放打断音乐的解决方案,在上一篇博客《uniapp实现音频播放抢占系统音频焦点》中已解决,实现的细节均已清晰指明。
由于音频的初始化加载,由原来的plus.audio.createPlayer
转为使用android.media.MediaPlayer
类来实现,引出问题3. 音频输出频道(扬声器/听筒)的切换问题
初始化播放器
先声明相关变量
data() {
return {
voicePlayer: null, // 语音播放器
watchProximity: null, // 设备距离监听器
sessionPlayMode: 0, // 语音播放模式 0扬声器 1听筒
wakeLock: null, // Android端唤醒锁
audioManager: null, // Android音频管理器
audioFocus: false // 音频聚焦状态
}
},
onLoad() {
plus.android.importClass('android.media.AudioManager');
}
初始化播放器
async initPlayer(src) {
return new Promise((resolve) => {
let path = plus.io.convertLocalFileSystemURL(src); // 本地文件路径转为系统路径
// 用于判断文件是否存在
plus.io.resolveLocalFileSystemURL(
path,
() => {
console.log('路径', path);
let MediaPlayer = plus.android.importClass('android.media.MediaPlayer');
let AudioAttributes = plus.android.importClass('android.media.AudioAttributes');
this.voicePlayer = new MediaPlayer();
let completionCB = plus.android.implements('android.media.MediaPlayer$OnCompletionListener', {
onCompletion: () => {
this.voicePlayEnded(); // 语音播放完毕
}
});
this.voicePlayer.setOnCompletionListener(completionCB);
this.voicePlayer.setDataSource(path);
this.voicePlayer.setAudioAttributes(AudioAttributes.CONTENT_TYPE_SPEECH); // 设置类型为语音
resolve(true);
},
(err) => {
// 文件获取失败
console.log('文件获取失败,请重新获取', err);
// 释放唤醒锁
if (this.wakeLock) {
this.wakeLock.release();
this.wakeLock = null;
}
// 销毁正在监听设备距离的监听器
if (this.watchProximity) {
plus.proximity.clearWatch(this.watchProximity);
this.watchProximity = null;
}
resolve(false);
}
);
});
}
播放语音
async playVoice(src) {
console.log('播放地址', src);
if (!(await this.initPlayer(src))) {
return;
}
let main = plus.android.runtimeMainActivity();
let Context = plus.android.importClass('android.content.Context');
this.audioManager = main.getSystemService(Context.AUDIO_SERVICE);
// 判断是否有音乐播放,若存在音乐播放,把音频聚焦到语音上
let isMusicActive = this.audioManager.isMusicActive();
if (isMusicActive) {
this.audioFocus = true;
// 请求音频焦点,暂停音乐播放,待语音播放完毕后恢复
this.audioManager.requestAudioFocus(null, this.audioManager.STREAM_MUSIC, this.audioManager.AUDIOFOCUS_GAIN_TRANSIENT);
}
// 设置播放模式
let headsetStatus = false;
let AudioDeviceInfo = plus.android.importClass('android.media.AudioDeviceInfo');
let audioDevices = this.audioManager.getDevices(this.audioManager.GET_DEVICES_OUTPUTS);
// 判断设备类型
for (let deviceInfo of audioDevices) {
let status = plus.android.invoke(deviceInfo, 'getType');
if (
status == AudioDeviceInfo.TYPE_WIRED_HEADPHONES ||
status == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
status == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP ||
status == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
) {
headsetStatus = true;
}
}
if (headsetStatus) {
// 有耳机
this.audioManager.setSpeakerphoneOn(false);
this.audioManager.setMode(this.audioManager.MODE_NORMAL);
this.voicePlayer.setAudioStreamType(this.audioManager.STREAM_MUSIC);
} else {
// 无耳机
if (!this.sessionPlayMode) {
// 扬声器
this.audioManager.setSpeakerphoneOn(true);
this.audioManager.setMode(this.audioManager.MODE_NORMAL);
this.voicePlayer.setAudioStreamType(this.audioManager.STREAM_MUSIC);
// 扬声器模式下,需要监听距离对声道进行实时修改
this.watchProximity = plus.proximity.watchProximity((distance) => {
// Android端接近为0,远离为5
this.voicePlayer.pause();
if (distance !== 0) {
// 扬声器
this.audioManager.setSpeakerphoneOn(true);
this.audioManager.setMode(this.audioManager.MODE_NORMAL);
} else {
// 听筒
this.voicePlayer.seekTo(0); // 播放进度调回0
this.audioManager.setSpeakerphoneOn(false);
this.audioManager.setMode(this.audioManager.MODE_IN_COMMUNICATION);
}
this.voicePlayer.start();
});
} else {
// 听筒
this.audioManager.setSpeakerphoneOn(false);
this.audioManager.setMode(this.audioManager.MODE_IN_COMMUNICATION);
this.voicePlayer.setAudioStreamType(this.audioManager.STREAM_VOICE_CALL);
}
// Android端需要设置唤醒模式才能在接近传感器激活时关闭屏幕
let PowerManager = plus.android.importClass('android.os.PowerManager');
let pm = main.getSystemService(Context.POWER_SERVICE);
// 32代表PROXIMITY_SCREEN_OFF_WAKE_LOCK,唤醒锁定电平:当接近传感器激活时关闭屏幕
let wakeStatus = pm.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK);
// 系统支持该唤醒模式
if (wakeStatus) {
this.wakeLock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, 'TAG');
this.wakeLock.acquire();
}
}
this.voicePlayer.prepare();
this.voicePlayer.start();
}
语音播放完毕
voicePlayEnded() {
// 销毁正在监听设备距离的监听器
if (this.watchProximity) {
plus.proximity.clearWatch(this.watchProximity);
this.watchProximity = null;
}
// 释放唤醒锁
if (this.wakeLock) {
this.wakeLock.release();
this.wakeLock = null;
}
// 放弃系统音频焦点
if (this.audioFocus) {
this.audioManager.setMode(this.audioManager.MODE_NORMAL);
this.audioManager.abandonAudioFocus(null);
this.audioFocus = false;
}
// 释放资源
this.voicePlayer.release();
this.voicePlayer = null;
}
分析总结
上述代码片段基本实现了语音播放的各种细节,以及解决文章开头的三个问题,调用仅需要this.playVoice('xxx')
传入语音文件路径即可。
针对问题1,使用音频焦点的请求聚焦及放弃,在播放音频时聚焦设置其持续时间为AUDIOFOCUS_GAIN_TRANSIENT
,用于指示临时增益或音频焦点的请求,预计会持续很短的时间;
针对问题2,使用音频管理类的getDevices
方法获取GET_DEVICES_OUTPUTS
输出设备,配合AudioDeviceInfo
类,判断输出设备的类型是否为耳机,最后配合问题3的解决方案即可实现耳机/扬声器的切换控制;
针对问题3,使用音频管理类的setSpeakerphoneOn
、setMode
方法来控制音频切换扬声器/听筒。
至此问题已全部解决,完美实现Android端
的语音播放功能,继续加油吧…
附上参考链接:AudioManager类、MediaPlayer类、AudioAttributes类、AudioDeviceInfo类