业务需求:根据后台返回消息播报语音,要求后台运行可继续播报
实现步骤:
- 建立socket连接监听消息,并建立心跳检测机制,防止socket意外断连
- 将接收到的文字信息转化为音频文件
- 使用uni.getBackgroundAudioManager(),实现后台运行可以持续播报音频
- 解决并发问题,同时接收多个文件按顺序播报
1. 建立socket连接
onShow() {
const SocketTask = getApp().globalData.SocketTask;
if (!SocketTask) {
this.linkSocket();
}
}
async linkSocket() {
// 连接Socket服务器
let SocketTask;
const TOKEN = store.getters['base/_token'];
await new Promise((resolve, reject) => {
SocketTask = uni.connectSocket({
url: wsUrl,
header: {
'content-type': 'application/json',
Authorization: TOKEN
},
success: () => {
console.log(`WebSocket connect成功`);
resolve();
},
fail: () => {
console.log('WebSocket connect失败');
reject();
}
});
});
SocketTask.id = this.randomInt(1000, 9999);
// 初始化心跳,心跳用于检测连接是否正常
SocketTask.heartCheck = { ...heartCheck };
SocketTask.reconnectObj = { ...reconnectObj };
// 赋值全局变量,用于判断是否已连接
getApp().globalData.SocketTask = this.SocketTask = SocketTask;
// 开始监听
this.initEventHandle(this.SocketTask);
}
randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
定义心跳对象
// ws 心跳对象
let heartCheck = {
timeout: 5000, // 收到pong后再次发ping的间隔时间;也是ws连接不上的最大等待时间 之后关掉连接并重连
timeoutObj: null,
serverTimeoutObj: null,
reset: function() {
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start: function(SocketTask) {
this.timeoutObj = setTimeout(() => {
SocketTask.send({ data: 'ping' });
this.serverTimeoutObj = setTimeout(() => {
SocketTask.close();
}, this.timeout);
}, this.timeout);
}
};
消息监听
initEventHandle(SocketTask) {
// 监听消息
SocketTask.onMessage(res => {
// 心跳检测,后台返回pong表示连接正常
if (res.data === 'pong') {
SocketTask.heartCheck.reset().start(SocketTask);
} else {
this.toAudioText = res.data;
// 接收到文字消息转语音
this.getMp3();
}
});
SocketTask.onOpen(() => {
console.log(`${SocketTask.id} WebSocket onOpen`);
SocketTask.heartCheck.reset().start(SocketTask);
SocketTask.heartCheck.isAlive = true;
});
SocketTask.onError(res => {
console.log(`${SocketTask.id} WebSocket onError`, res);
this.reconnect(SocketTask);
});
SocketTask.onClose(res => {
console.log(`${SocketTask.id} WebSocket onClose`, res);
SocketTask.heartCheck.isAlive = false;
if (res.code === 4000 || res.code === 4001) {
console.log(`${SocketTask.id} WebSocket ${res.code}关闭`);
} else {
// 非正常关闭 重连
this.reconnect(SocketTask);
}
});
}
重连机制
let reconnectObj = {
timeout: 5000, // 重连间隔时间
timer: null, // 重连计时器
lock: false, // 重连锁
limit: 0 // 重连最多次数
};
...
reconnect(SocketTask) {
if (SocketTask.reconnectObj.lock) return;
SocketTask.reconnectObj.lock = true;
clearTimeout(SocketTask.reconnectObj.timer);
if (SocketTask.reconnectObj.limit < 24) {
SocketTask.reconnectObj.timer = setTimeout(() => {
this.linkSocket();
SocketTask.reconnectObj.lock = false;
}, SocketTask.reconnectObj.timeout);
SocketTask.reconnectObj.limit++;
} else {
console.log(`${SocketTask.id}WebSocket reach limit 24!!!!!!!`);
uni.showToast({
title: '很抱歉,您与服务器失去连接,请重启小程序',
icon: 'none'
});
}
}
2.使用零七生活API完成文字转语音,api返回二进制音频流,直接在小程序无法播放,需要使用writeFile写入文件后可播放,注意播放完成后使用fs.unlink删除本地文件
const fs = uni.getFileSystemManager(); // 文件管理器API
...
getMp3() {
const target = `${wx.env.USER_DATA_PATH}/${new Date().getTime()}.mp3`;
return new Promise((resolve, reject) => {
uni.request({
url: `https://api.oick.cn/txt/apiz.php?text=${this.toAudioText}&spd=5`,
method: 'GET',
responseType: 'arraybuffer', // 注意此处,否则返回音频格式不可用
success: (res: any) => {
fs.writeFile({
filePath: target,
data: res.data,
encoding: 'binary',
success: res => {
// 将接收到的消息推进消息队列
this.playAudio.push(target);
},
fail: err => {
console.log(err);
}
});
},
fail: (err: any) => {
reject(err);
}
});
});
}
3. 播放背景音频:注意需要在 app.json 中配置 requiredBackgroundModes
属性
为了实现锁屏也可接收消息,进入页面后开始循环播放空音频,保证音频播放不中断,接收到消息后播放消息队列;
const backgroundAudioManager = uni.getBackgroundAudioManager();
...
onShow() {
//循环播放音频
this.playInit();
}
playInit() {
backgroundAudioManager.title = '语音播报';
backgroundAudioManager.src = kong;
backgroundAudioManager.onEnded(() => {
if (this.index < this.playAudio.length) {
console.log('播放列表:', this.playAudio);
backgroundAudioManager.src = this.playAudio[this.index];
// backgroundAudioManager.src = ding;
this.index++;
} else {
backgroundAudioManager.src = kong;
// 清空播放列表
if (this.index > 0) {
this.clearFile();
this.playAudio = [];
this.index = 0;
}
}
});
}
4.解决并发问题,定义playAudio数组用于存放音频队列,当接收多个消息时,将消息推入数组,当播放完一个音频后,检测数组是否为空,如果不为空,播放消息音频,否则继续播放空音频。