本文主要是介绍微信小程序实现websokect语音对话,实现后端实时返回片段音频,前端播放+心跳检测,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
上一篇微信小程序实现和AI语音对话功能
1.目的:之前项目实现跟ai语音对话,因为API语音结果生成缓慢,返给前端大概在10左右,所以领导要求使用websokect,实时接受后端反的片段音频,前端播放。这样生成的时间就会快很多。
2.代码片段
// pages/ai/aiVoice/index.ts
import {timeExChange,copyText,openSetting,startMonitoringNetwork,
} from '../../../utils/utils'
//引入插件:微信同声传译
const plugin = requirePlugin('WechatSI');
//获取全局唯一的语音识别管理器recordRecoManager
const manager = plugin.getRecordRecognitionManager();
// const VUE_APP_API_URL_MSG = "wss://地址/";
let isOpen = false; // 使用布尔值来表示连接状态
let socketTask = null;
let reconnectAttempts = 0;
let maxReconnectAttempts = 10; // 最大重连次数
let reconnectTimer = null;
let reconnectInterval = 1000;//重置重连间隔
Page({/*** 页面的初始数据*/innerAudioContext: null,data: {audioCtx: null,// 存放从后端接收到的语音地址数组voiceUrls: [],// 当前播放的语音索引currentVoiceIndex: 0,startTime: 0,endTime: 0,elapsedTime: 0, //计时器isplay: true,onstops: true, //默认执行onStopisFlag: false, //是否点击录音到获取结果之间状态longPressTimer: null, // 用于存储长按定时器的变量touchStartTime: 0, //长按开始touchEndTime: 0, //松开结束 authsetting: false, //是否获取授权url: "地址",openid: null,islongPress: false, //是否长按ParentValue: 'Parent',loginShow: false, //登录弹窗默认关闭annimationFlag: false, //logo动画默认关闭touchstart: false, //默认没有按下toView: null,scrollTop: 0,src: '', //语音地址resultobj: {result: "",tempFilePath: ""},msgText: 1, //1默认初始化 2对话进行中 3结束对话 4对话出现问题flag: 1,haveflag: false, //防止重复点击recordState: false, //麦克风默认关闭状态msglist: [], //聊天记录showModal: false, //历时消息记录heartBeatIntervalId:null,},/*** 生命周期函数--监听页面加载*/onLoad() {// 关闭主页按钮wx.hideHomeButton();wx.setNavigationBarTitle({title: "AI语音对话"})// 获取语音授权this.getSeeting(1);//识别语音this.initRecord();const authset = wx.getStorageSync('AUTHSETTING');// console.log("初始化语音权限", authset);if (!authset) { //没有获取录音权限// 重新获取录音权限this.getSeeting(1);}},/*** 生命周期函数--监听页面初次渲染完成*/onReady() {},/*** 生命周期函数--监听页面显示*/onShow() {reconnectAttempts = 0;//检测网络情况startMonitoringNetwork();// 判断用户是否登录this.isLogin();},// 链接sokectconnectWebSocket() {let that = this;console.log("拿到openidlema", wx.getStorageSync('OPENID'))if (socketTask) {// 如果已有 WebSocket 连接,则关闭它socketTask.close();}socketTask = wx.connectSocket({url: VUE_APP_API_URL_MSG + wx.getStorageSync('OPENID'),header: {'content-type': 'application/json'},// protocols: ['protocolOne', 'protocolTwo'] // 可选,使用特定协议});socketTask.onOpen(function (res) {console.log('WebSocket连接成功', socketTask);isOpen = true; // 更新连接状态reconnectAttempts = 0; // 连接成功后重置重连尝试次数reconnectInterval = 1000; // 连接成功后重置重连间隔that.startHeartbeat(); // 启动心跳机制});socketTask.onMessage(function (res) {console.log("获取消息成功", res);if(res.data=="ping"){return false}if (that.data.isplay) {wx.hideLoading();//设置语音that.setData({isFlag: false,})const result = res.data;// console.log("处理过后的数据", result);that.data.voiceUrls.push(result);// 判断是不是第一次收到消息且目前没有播放===才调用播放音频方法if (that.data.voiceUrls.length == 1 && !that.data.audioCtx) {console.log("第一次调用")that.setData({msgText: 2, //正在对话`annimationFlag: true,haveflag: true})that.playNextVoice();}} else {wx.hideLoading();wx.showToast({title: res.data.msg,icon: 'none',duration: 2000})that.setData({msgText: 1,voiceUrls: []})}});socketTask.onClose(function (res) {console.log('WebSocket连接已关闭', res);that.socketClose();// isOpen = false; // 更新连接状态// socketTask = null; // 清除 WebSocket 实例that.stopHeartbeat(); // 停止心跳机制// that.reconnectWebSocket(); // 尝试重连});socketTask.onError(function (res) {console.log('WebSocket连接错误:', res);isOpen = false; // 更新连接状态that.stopHeartbeat(); // 停止心跳机制that.reconnectWebSocket(); // 尝试重连});},// 启动心跳机制
startHeartbeat() {let that = this;if (isOpen) {that.data.heartBeatIntervalId = setInterval(() => {if (isOpen) {console.log('发送心跳消息');let data = {msg:"ping"}socketTask.send({data: JSON.stringify(data),success(res) {console.log('心跳消息发送成功');}});} else {clearInterval(that.data.heartBeatIntervalId);}}, 60000); // 每 10 秒发送一次心跳消息}
},// 停止心跳机制
stopHeartbeat() {if (this.data.heartBeatIntervalId) {console.log("停止心跳机制");clearInterval(this.data.heartBeatIntervalId);this.data.heartBeatIntervalId = null; // 清除定时器 IDthis.setData({msgText: 1, //初始化annimationFlag: false,haveflag: false,touchstart: false})}
},// 尝试重新连接 WebSocket
reconnectWebSocket() {if (!reconnectTimer) { // 确保没有重复的重连定时器reconnectTimer = setTimeout(() => {console.log('尝试重新连接 WebSocket');this.connectWebSocket(); // 重新初始化 WebSocketreconnectTimer = null; // 清除重连定时器 ID}, 6000); // 重连间隔时间(毫秒)}
},// 播放下一个音频playNextVoice() {if (this.data.voiceUrls.length > 0) {const url = this.data.voiceUrls.shift();this.playVoice(url);} else {this.setData({audioCtx: null,msgText: 1,annimationFlag: false,haveflag: false,src: ""});}},// 播放音频playVoice(url) {if (!this.data.audioCtx) {this.data.audioCtx = wx.createInnerAudioContext();this.data.audioCtx.onError((res) => {console.log("音频播放错误", res);this.setData({msgText: 1,annimationFlag: false,haveflag: false,touchstart: false});});this.data.audioCtx.onEnded(() => {this.playNextVoice();});}this.data.audioCtx.src = url;this.data.audioCtx.play();},// 预加载音频preloadVoices(voiceUrls) {voiceUrls.forEach(url => {const audio = wx.createInnerAudioContext();audio.src = url;audio.preload = 'auto'; // 设置为自动预加载audio.onCanplay(() => {console.log(`Audio ${url} 预加载`);});audio.onError((res) => {console.error(`错误预加载 ${url}`, res);});});},// 初始化播放音频playVoices(voiceUrls) {this.preloadVoices(voiceUrls); // 预加载音频this.setData({voiceUrls}); // 更新音频列表this.playNextVoice(); // 开始播放音频},playVoice(url) {if (!this.data.audioCtx) {this.data.audioCtx = wx.createInnerAudioContext();this.data.audioCtx.onError((res) => {console.log("音频播放错误", res);this.setData({msgText: 1,annimationFlag: false,haveflag: false,touchstart: false});});this.data.audioCtx.onEnded(() => {this.playNextVoice();});}this.data.audioCtx.src = url;this.data.audioCtx.play();},// 关闭当前正在播放的语音stopVoice() {if (this.data.audioCtx) {this.data.audioCtx.stop(); // 停止播放this.data.audioCtx.destroy(); // 销毁音频上下文,释放资源this.setData({voiceUrls: [],audioCtx: null,msgText: 1, //初始化annimationFlag: false,haveflag: false,src: ""});}},// 发送数据wsSend() {// 参数let obj = {question: this.data.resultobj.result,// question: "请给我写300字的有关妈妈的作文",openId: wx.getStorageSync('OPENID'),}console.log("发送数据", obj)socketTask.send({data: JSON.stringify(obj),success(res) {// wx.hideLoading();console.log('发送数据帧成功', res);},fail(res) {wx.hideLoading();console.log('发送数据帧失败', res);}})},// 重连websokectattemptReconnect() {if (reconnectAttempts >= maxReconnectAttempts) {console.error('已达最大重连次数,停止重连');return;}reconnectAttempts++;reconnectInterval *= 2; // 指数退避,每次重连间隔加倍console.log(`尝试重新连接,间隔 ${reconnectInterval} 毫秒`);setTimeout(() => {this.connectWebSocket();}, reconnectInterval);},// 关闭sokectsocketClose() {console.log("关闭sokect",socketTask)if (socketTask) {socketTask.close(); // 关闭 WebSocket 连接socketTask = null; // 清除 WebSocket 实例isOpen = false; // 更新连接状态reconnectTimer = null;reconnectAttempts = 0;reconnectInterval = 1000;this.setData({heartBeatIntervalId:null})}},// 判断用户是否登录isLogin() {// 获取本地存储中的 OPENIDconst openid = wx.getStorageSync('OPENID');if (openid) {// 如果本地存储中存在 OPENID,则说明用户已经登录过,可以直接使用 OPENID 进行后续操作this.setData({loginShow: false})console.log("连接 WebSocket")// 连接 WebSocketthis.connectWebSocket();// 这里可以进行其他操作,比如直接跳转到主页面} else {// 如果本地存储中没有 OPENID,则需要调用登录接口获取 OPENIDthis.setData({loginShow: true,openid: openid})}},onChildEvent(event) {this.setData({loginShow: event.detail.login_show})// 连接 WebSocketthis.connectWebSocket();},//暂停语音backIndex() {let that = this;// 关闭音频this.stopVoice();// that.innerAudioContext.src = "";// that.innerAudioContext.stop(); //暂停音频that.setData({msgText: 1, //初始化touchstart: false, //按钮恢复初始状态annimationFlag: false,haveflag: false,'resultobj.tempFilePath': "",isplay: false,resultText: "",src: ""})// 如何判断当前是语音录制识别状态console.log("暂停语音", that.data.isFlag)if (that.data.isFlag) {that.setData({onstops: false, //是否执行onStop})wx.showLoading({title: '关闭中...',icon: 'none',mask: true})// 停止识别manager.stop();}wx.vibrateShort({type: 'heavy',fail: function (err) {console.error('短振动失败', err)}})},//识别语音 -- 初始化initRecord() {const that = this;// 有新的识别内容返回,则会调用此事件manager.onRecognize = function (res) {console.log("有新的识别内容返回,则会调用此事件")}// 正常开始录音识别时会调用此事件manager.onStart = function (res) {console.log("成功开始录音识别", res)that.setData({// annimationFlag:true})}//识别结束事件manager.onStop = function (res) {if (!that.data.isplay) {wx.showToast({title: "听不清楚,请再说一遍!",icon: 'success',image: '/assets/image/no_voice.png',duration: 1000,success: function (res) {that.setData({haveflag: false,isFlag: false})},fail: function (res) {console.log(res);}});return false}if (res.result == '') {wx.hideLoading();// wx.showToast({// title: '听不清楚,请重新说一遍!',// icon: 'none',// duration: 2000// })// that.setData({// msgText: 1, //初始化// haveflag: false,// isFlag: false,// })that.showRecordEmptyTip()return;} else {// wx.showLoading({// title: '正在思考...',// icon: 'none',// })console.log("获取翻译", res.result)that.setData({resultobj: {result: res.result,tempFilePath: res.tempFilePath,},msgText: 2, //正在对话// annimationFlag: true})// 调用接口// that.resultTextApi();// 给websoket发送数据that.wsSend();}}// 识别错误事件manager.onError = function (res) {console.log("error msg", res);wx.hideLoading();wx.showToast({icon: "none",title: '请重新开始~'})that.setData({haveflag: false,msgText: 1,annimationFlag: false,isFlag: false, //当前录制语音识别状态touchstart: false})}},// 根据wx.getSetting判断用户是否打开了录音权限,如果没有打开,则通过wx.authorize,向用户打开授权请求,如果用户拒绝了,就给用户打开授权设置页面。getSeeting(type) {// wx.showLoading({// title: '获取录音权限',// icon: 'none',// mask: true// })const _this = thiswx.getSetting({ //获取用户当前设置success: res => {// wx.hideLoading();// console.log('获取权限', res);if (res.authSetting['scope.record']) { //查看是否授权了录音设置// console.log('获取权限1111');const authset = wx.setStorageSync('AUTHSETTING', true);_this.setData({authsetting: true})if (type == 2) {wx.showToast({title: '获取录音权限成功,点击重新开始!',icon: 'none',duration: 2000})}} else {// 用户还没有授权,向 用户发起授权请求wx.authorize({ //提前向用户发起授权请求,调用后会立刻弹窗询问用户是否同意授权小程序使用某项功能或获取用户的某些数据,但不会实际调用对应接口scope: 'scope.record',success() { //用户同意授权摄像头// console.log("同意授权");// wx.showToast({// title: '获取录音权限成功',// icon: 'none',// duration: 2000// })},fail() { //用户不同意授权摄像头openSetting()}})}},fail() {// console.log('获取用户授权信息失败');wx.showToast({title: '获取权限失败',icon: 'none',duration: 2000})}})},//获取文字接口resultTextApi() {//调用接口const formdata = {question: this.data.resultobj.result,// question:"请给我写300字的有关妈妈的作文",openId: wx.getStorageSync('OPENID'),};const requestTask = wx.request({url: this.data.url + '/cyjgVoice/ans',method: 'POST',//请求头需要改为stream模式header: {'content-type': 'application/x-www-form-urlencoded' // 或者 'application/json' 如果你的后端能接受JSON格式},data: formdata, // 直接使用formdata对象success: res => {console.log("语音1111", res);}})},//语音 --按住说话touchStart(e) {// 判断是否获取录音权限// if (!(this.data.authsetting)) {// // console.log("获取语音权限")// this.getSeeting(2);// return// }this.setData({resultobj: {result: "",tempFilePath: "",}})console.log('按住说话', isOpen);if(!isOpen){//代表websokect未连接上wx.showToast({title: '正在建立语音通信,稍后请重试',icon: 'none',duration: 3000})return false}if (this.data.haveflag) { //true 请先结束语音console.log("没关闭haveflag")wx.showToast({title: '请先关闭语音!',icon: 'none',duration: 2000})return false}// 当前正在识别语音,还没结束上一次识别,请先关闭再进行录音if (this.data.isFlag) { //true 请先结束语音console.log("没关闭isFlag")wx.showToast({title: '请先关闭语音!',icon: 'none',duration: 2000})return false}// this.innerAudioContext.src = "";// this.innerAudioContext.stop(); //暂停音频wx.vibrateShort({type: 'heavy',fail: function (err) {console.error('短振动失败', err);}})this.setData({islongPress: true,isplay: true})var flag = Number(e.currentTarget.dataset.flag)this.setData({recordState: true, //录音状态flag: flag,touchstart: true, //按下msgText: 2, //初始化状态})// 语音开始识别manager.start({lang: 'zh_CN', // 识别的语言})},//语音 --松开结束touchEnd(e) {if (!(this.data.islongPress)) { //如果是长按执行下面内容console.log('松开1')return false}wx.showLoading({title: '正在思考...',icon: 'none',})if (this.data.haveflag) { //true 请先结束语音console.log('松开2');this.setData({touchstart: false,})wx.hideLoading();// wx.showToast({// title: '请先关闭语音111!',// icon: 'none',// duration: 2000// })return false}console.log('松开结束3');this.setData({touchstart: false,recordState: false,islongPress: false, //长按初始状态isFlag: true, //判断从松手到识别录音期间状态haveflag: true})// this.resultTextApi();// this.wsSend();// 语音结束识别manager.stop();},// 关闭弹窗closeModal() {this.setData({showModal: false})},showRecordEmptyTip() {console.log("请说话1")this.setData({msgText: 1, //初始化haveflag: false,isFlag: false,})wx.showToast({title: "听不清楚,请再说一遍!",icon: 'success',image: '/assets/image/no_voice.png',duration: 1000,success: function (res) {},fail: function (res) {console.log(res);}});},/*** 生命周期函数--监听页面隐藏*/onHide() {// this.innerAudioContext.stop();reconnectAttempts = 0;this.stopVoice();this.socketClose();reconnectTimer = null;socketTask.close();},/*** 生命周期函数--监听页面卸载*/onUnload() {// this.innerAudioContext.stop();reconnectAttempts = 0;this.stopVoice();this.socketClose();reconnectTimer = null;socketTask.close();},/*** 页面相关事件处理函数--监听用户下拉动作*/onPullDownRefresh() {},/*** 页面上拉触底事件的处理函数*/onReachBottom() {},/*** 用户点击右上角分享*/onShareAppMessage() {}
})
这篇关于微信小程序实现websokect语音对话,实现后端实时返回片段音频,前端播放+心跳检测的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!