业务需求:根据后台返回消息播报语音,要求后台运行可继续播报

实现步骤:

  1. 建立socket连接监听消息,并建立心跳检测机制,防止socket意外断连
  2. 将接收到的文字信息转化为音频文件
  3. 使用uni.getBackgroundAudioManager(),实现后台运行可以持续播报音频
  4. 解决并发问题,同时接收多个文件按顺序播报

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 属性

uniapp ios 获取语音权限 uniapp实时语音_uniapp ios 获取语音权限

为了实现锁屏也可接收消息,进入页面后开始循环播放空音频,保证音频播放不中断,接收到消息后播放消息队列;

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数组用于存放音频队列,当接收多个消息时,将消息推入数组,当播放完一个音频后,检测数组是否为空,如果不为空,播放消息音频,否则继续播放空音频。