From 21b36468205774b16f6ca9da385217e7fbc85f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E5=B2=A9?= Date: Fri, 5 Dec 2025 17:37:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=80=E5=8F=91ai=E8=81=8A=E5=A4=A9=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 8 +- src/pages.json | 1 + .../sparring/components/talk-microphone.vue | 358 +++++++++++++ .../sparring/components/talk-speaker.vue | 99 ++++ src/pages/sparring/js/StreamPlayer.js | 1 - src/pages/sparring/js/useMicrophone.js | 149 ++++++ src/pages/sparring/js/useSpeaker.js | 129 +++++ src/pages/sparring/js/useWebSocket.js | 118 +++++ src/pages/sparring/talk.vue | 407 ++++++--------- src/pages/sparring/talk4.vue | 492 ++++++++++++++++++ src/pages/sparring/tip.vue | 3 + src/pagesB.json | 1 + .../yao-RecordFrame/yao-RecordFrame.vue | 1 + 13 files changed, 1511 insertions(+), 256 deletions(-) create mode 100644 src/pages/sparring/components/talk-microphone.vue create mode 100644 src/pages/sparring/components/talk-speaker.vue create mode 100644 src/pages/sparring/js/useMicrophone.js create mode 100644 src/pages/sparring/js/useSpeaker.js create mode 100644 src/pages/sparring/js/useWebSocket.js create mode 100644 src/pages/sparring/talk4.vue diff --git a/.env.development b/.env.development index cf4545bf..f235ef78 100644 --- a/.env.development +++ b/.env.development @@ -5,19 +5,17 @@ ENV = 'development' # VITE_APP_BASE_API_Url = 'https://aits.jlbank.com.cn:7001' -#VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002' - - +VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7001' # VITE_APP_BASE_API_Url = 'https://aitstest.jlbank.com.cn:7002' # VITE_APP_BASE_API_Url = 'http://192.168.247.200' # VITE_APP_BASE_API_Url = 'http://aitscdn.jlbank.com.cn:7001' # VITE_APP_BASE_API_Url = 'http://192.168.108.129' # dev -# VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001' +#VITE_APP_BASE_API_Url = 'http://25.18.122.65:7001' # sit -VITE_APP_BASE_API_Url = 'http://25.18.122.91:9786' +#VITE_APP_BASE_API_Url = 'http://25.18.122.91:9786' # UAT # VITE_APP_BASE_API_Url = 'http://25.18.122.78:9786' diff --git a/src/pages.json b/src/pages.json index 62ee516d..68ecbebe 100644 --- a/src/pages.json +++ b/src/pages.json @@ -516,6 +516,7 @@ "path": "pages/sparring/talk", "style": { "navigationBarTitleText": "AI陪练通话", + "disableSwipeBack": true, "app-plus": { "titleNView": false } diff --git a/src/pages/sparring/components/talk-microphone.vue b/src/pages/sparring/components/talk-microphone.vue new file mode 100644 index 00000000..735be302 --- /dev/null +++ b/src/pages/sparring/components/talk-microphone.vue @@ -0,0 +1,358 @@ + + + + + + \ No newline at end of file diff --git a/src/pages/sparring/components/talk-speaker.vue b/src/pages/sparring/components/talk-speaker.vue new file mode 100644 index 00000000..ed364e38 --- /dev/null +++ b/src/pages/sparring/components/talk-speaker.vue @@ -0,0 +1,99 @@ + + + + + + diff --git a/src/pages/sparring/js/StreamPlayer.js b/src/pages/sparring/js/StreamPlayer.js index f0340d68..0e1c7bce 100644 --- a/src/pages/sparring/js/StreamPlayer.js +++ b/src/pages/sparring/js/StreamPlayer.js @@ -30,7 +30,6 @@ export class StreamPlayer { } appendChunk(audioData) { - console.log('this.audioQueue', this.audioQueue.length); this.audioQueue.push(audioData); this._processQueue(); } diff --git a/src/pages/sparring/js/useMicrophone.js b/src/pages/sparring/js/useMicrophone.js new file mode 100644 index 00000000..af6863c2 --- /dev/null +++ b/src/pages/sparring/js/useMicrophone.js @@ -0,0 +1,149 @@ +import { + ref, + onUnmounted, + watch +} from 'vue'; + import { + ProtocolCodec, + MessageType, + SerializationType, + CompressionType, + ControlCommand, + ProtocolConst + } from './ProtocolCodec'; +/** + * 麦克风操作Hook(封装开始/停止录音、状态管理) + * @param {Object} options - 录音配置项 + * @param {number} options.duration - 最大录音时长(秒,默认60) + * @param {string} options.format - 录音格式(mp3/wav/pcm,默认mp3) + * @param {number} options.sampleRate - 采样率(默认16000) + * @param {number} options.bitRate - 码率(默认16000) + * @param {Function} options.onData - 实时音频数据回调(仅H5/PCM格式生效) + * @param {Function} options.onStop - 结束回调 + * @param {Function} options.onError - 错误回调 + * @returns {Object} 录音控制方法+状态 + */ +export default function useMicrophone(options = {}) { + console.log('options', options); + const send = options.send + const recordFrame = ref(null) + const renderJSModule = ref(null) + + // 默认配置 + const defaultOptions = { + duration: 60, + format: 'mp3', + sampleRate: 16000, + bitRate: 16000, + onData: () => {}, + onStop: () => {}, + onError: (err) => console.error('录音错误:', err) + }; + const finalOptions = { + ...defaultOptions, + ...options + }; + + // 响应式状态 + const isRecording = ref(false); // 是否正在录音 + const recordTime = ref(0); // 录音时长(秒) + const recordResult = ref(null); // 录音结果(临时文件路径/Blob) + const recorderManager = ref(null); // 录音管理器实例(uniapp/小程序) + const audioContext = ref(null); // AudioContext(H5) + const mediaRecorder = ref(null); // MediaRecorder(H5) + const timer = ref(null); // 录音时长定时器 + + // 平台判断 + const platform = uni.getSystemInfoSync().platform; + + // ==================== 核心方法:开始录音 ==================== + const startRecording = async () => { + // 防止重复点击 + if (isRecording.value) return; + + // 重置状态 + recordTime.value = 0; + recordResult.value = null; + console.log('开始', recordFrame.value); + recordFrame.value.start({ + sampleRate: 16000, + frameSize: 1024, + gain: 1.0, + onFrameRecorded: ({ isLastFrame, frameBuffer }) => { + // console.log('录音返回', frameBuffer); + const data = ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer) + // console.log('录音返回', data); + send(data) + // try { + // ws.value.send({ + // data: ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer) + // }); + // } catch (e) {} + }, + onDecibels: (decibels) => { + // this.currentDecibels = decibels; + } + }); + // this.isRecording = true; + // this.status = '录音中...'; + // isRecording.value = true; + + }; + + // ==================== 核心方法:停止录音 ==================== + const stopRecord = () => { + // if (!isRecording.value) return; + + // try { + // if (isH5) { + // mediaRecorder.value?.stop(); + // } else if (isMiniProgram || platform === 'app-plus') { + // recorderManager.value?.stop(); + // } + // isRecording.value = false; + // clearInterval(timer.value); + // } catch (err) { + // finalOptions.onError(`停止录音失败:${err.message}`); + // } + }; + + // ==================== 资源清理 ==================== + const cleanup = () => { + // stopRecord(); + // // H5端额外清理 + // if (isH5) { + // if (mediaRecorder.value) mediaRecorder.value = null; + // if (audioContext.value) audioContext.value.close(); + // // 释放Blob URL + // if (recordResult.value?.url) { + // URL.revokeObjectURL(recordResult.value.url); + // } + // } + // // 小程序/APP端清理 + // if (recorderManager.value) recorderManager.value = null; + }; + + // 组件卸载时自动清理 + // onUnmounted(() => { + // cleanup(); + // }); + + // 监听最大时长,自动停止 + // watch( + // () => recordTime.value, + // (val) => { + // if (val >= finalOptions.duration) { + // stopRecord(); + // } + // } + // ); + + // 返回控制方法+状态 + return { + + isRecording, + renderJSModule, + startRecording, + recordFrame + }; +}; \ No newline at end of file diff --git a/src/pages/sparring/js/useSpeaker.js b/src/pages/sparring/js/useSpeaker.js new file mode 100644 index 00000000..568ca088 --- /dev/null +++ b/src/pages/sparring/js/useSpeaker.js @@ -0,0 +1,129 @@ +import { + ref, + onUnmounted, + watch +} from 'vue'; + +/** + * 扬声器管理Hook(音频输出) + * @param {Object} options - 播放配置 + * @param {number} options.sampleRate - 播放采样率(默认16000,需与音频源一致) + * @param {number} options.volume - 初始音量(0~1,默认1) + * @param {boolean} options.autoPlay - 是否自动播放(默认false,需用户交互触发) + * @param {Function} options.onPlay - 播放开始回调 + * @param {Function} options.onError - 播放错误回调 + * @returns {Object} 播放控制方法+状态 + */ +export const useSpeaker = (options = {}) => { + // 默认配置 + const defaultOptions = { + sampleRate: 16000, + volume: 1.0, + autoPlay: false, + onPlay: () => {}, + onError: (err) => console.error('扬声器播放错误:', err) + }; + const finalOptions = { + ...defaultOptions, + ...options + }; + + // 响应式状态 + const isPlaying = ref(false); // 是否正在播放 + const volume = ref(finalOptions.volume); // 播放音量(0~1) + const audioContext = ref(null); // 音频播放上下文(H5) + const audioBufferSource = ref(null); // 音频缓冲源(播放PCM流) + const audioElement = ref(null); // 音频元素(播放文件/URL) + const playQueue = ref([]); // 音频数据播放队列(适配实时流) + + // 平台判断 + const isH5 = uni.getSystemInfoSync().platform === 'h5'; + + // ==================== 初始化播放上下文 ==================== + const initAudioContext = () => { + + }; + + // ==================== 核心方法:播放PCM音频流(实时) ==================== + /** + * 播放PCM音频数据(适配WebSocket传输的实时流) + * @param {ArrayBuffer} pcmData - PCM音频数据(16位单声道) + */ + const playPCM = (pcmData) => { + + + + }; + + // ==================== 控制方法:暂停/停止/调节音量 ==================== + const pausePlay = () => { + if (!isPlaying.value) return; + + if (isH5) { + if (audioBufferSource.value) { + audioBufferSource.value.stop(); + audioBufferSource.value = null; + } + audioElement.value?.pause(); + } else { + uni.createInnerAudioContext().pause(); + } + isPlaying.value = false; + }; + + const stopPlay = () => { + pausePlay(); + playQueue.value = []; // 清空播放队列 + if (audioContext.value) { + audioContext.value.close().then(() => { + audioContext.value = null; + }); + } + audioElement.value = null; + }; + + const setVolume = (val) => { + if (val < 0) val = 0; + if (val > 1) val = 1; + volume.value = val; + + if (isH5) { + audioContext.value?.destination.setVolume(val); + audioElement.value.volume = val; + } else { + uni.createInnerAudioContext().volume = val; + } + }; + + // ==================== 资源清理 ==================== + const cleanup = () => { + stopPlay(); + isPlaying.value = false; + volume.value = finalOptions.volume; + playQueue.value = []; + }; + + // 组件卸载时自动清理 + onUnmounted(() => { + cleanup(); + }); + + // 监听音量变化,同步更新 + watch(volume, (newVal) => { + setVolume(newVal); + }); + + // 返回控制方法+状态 + return { + // 响应式状态 + isPlaying, // 是否正在播放 + volume, // 当前音量(0~1) + // 核心方法 + playPCM, // 播放实时PCM音频流(适配WebSocket) + playAudio, // 播放音频文件/URL + pausePlay, // 暂停播放 + stopPlay, // 停止播放 + setVolume, // 调节音量(0~1) + cleanup // 手动清理资源 + }; +}; \ No newline at end of file diff --git a/src/pages/sparring/js/useWebSocket.js b/src/pages/sparring/js/useWebSocket.js new file mode 100644 index 00000000..c5d5d02d --- /dev/null +++ b/src/pages/sparring/js/useWebSocket.js @@ -0,0 +1,118 @@ +import { + getToken +} from '@/common/common' +import { + get_base_url +} from '@/api/request.js' +import { + ref, + onUnmounted, + computed, + getCurrentInstance +} from 'vue'; +import { + ProtocolCodec, + MessageType, + SerializationType, + CompressionType, + ControlCommand, + ProtocolConst +} from './ProtocolCodec'; +const DEFAULT_AUTH_PARAMS = { + user_id: '1001', // 默认用户ID(可从缓存/全局状态取) + token: '', // 优先从缓存获取,避免硬编码 + name: '测试用户', + device_id: uni.getSystemInfoSync().deviceId || '', // 设备ID(补充) + timestamp: Date.now() // 时间戳(防篡改) +}; +// const wsUrl = 'ws://127.0.0.1:8000/ws/audio' +const url = '/trapractice/voiceCallSocket/voiceCall' +const _baseUrl = get_base_url(url).split('http').join('ws') +export default function useWebSocket(aaas = null, options = {}) { + const isRecording = ref(false) //是否接通 + const ws = ref(null) + const initConnection = (authParams = {}, onStartRecord, sssssss) => { + const wsUrl = _baseUrl + url + '?summary=' + getToken() + console.log('wsUrl', wsUrl); + const finalAuthParams = { + ...DEFAULT_AUTH_PARAMS, + ...authParams, + }; + ws.value = uni.connectSocket({ + url: wsUrl, + fail: () => { + console.log('fail'); + }, + success: () => { + console.log('web'); + }, + fail: () => { + console.log('fail'); + } + }); + ws.value.onOpen((res) => { + ws.value.send({ + data: ProtocolCodec.pack(MessageType.IDENTITY, finalAuthParams) + }); + }); + ws.value.onMessage((res) => { + const { + msgType, + body + } = ProtocolCodec.unpack(res.data); + switch (msgType) { + case MessageType.IDENTITY: + console.log('身份校验', body); + onStartRecord(body) + break; + case MessageType.AUDIO_DATA: + const buffer = body; + if (buffer.byteLength < 2) break; + const uint8 = new Uint8Array(buffer); + const numArray = Array.from(uint8); + // currentPCMChunk.value = numArray; + sssssss(numArray) + // console.log('音频消息', body); + break; + case MessageType.ERROR: + + // this.handleErrorMessage(body); + break; + default: + console.log('其他消息', body); + break; + } + }); + } + const send = (data) => { + ws.value.send({ + data + }); + } + const close = () => { + ws.value.close({ + code: 1000, + reason: '用户挂断', + success: () => { + console.log('挂断success'); + }, + fail: () => { + console.log('挂断fail'); + }, + complete: () => { + console.log('挂断complete'); + } + + }); + } + + + + return { + ws, + close, + initConnection, + send, + isRecording + }; +} \ No newline at end of file diff --git a/src/pages/sparring/talk.vue b/src/pages/sparring/talk.vue index 7947f860..fcbeecb2 100644 --- a/src/pages/sparring/talk.vue +++ b/src/pages/sparring/talk.vue @@ -1,8 +1,8 @@ - - diff --git a/src/pages/sparring/tip.vue b/src/pages/sparring/tip.vue index 38bd37a1..3ccb5315 100644 --- a/src/pages/sparring/tip.vue +++ b/src/pages/sparring/tip.vue @@ -234,6 +234,9 @@ // #endif } const toTalking = () => { + common.setPageCache({ + sceneDesc:detailInfo.value.sceneDesc + }) common.navigateTo('/pages/sparring/talk?traId=' + traId.value + '&chaId=' + chaId.value + '&type=' + talkingType .value + '&isPreview=' + (isPreview.value ? '1' : '0')) } diff --git a/src/pagesB.json b/src/pagesB.json index 2b2f0445..5fa2084c 100644 --- a/src/pagesB.json +++ b/src/pagesB.json @@ -95,6 +95,7 @@ "path": "pages/sparring/talk", "style": { "navigationBarTitleText": "AI陪练通话", + "disableSwipeBack": true, "app-plus": { "titleNView": false } diff --git a/src/uni_modules/yao-RecordFrame/components/yao-RecordFrame/yao-RecordFrame.vue b/src/uni_modules/yao-RecordFrame/components/yao-RecordFrame/yao-RecordFrame.vue index 735be302..fb373ea7 100644 --- a/src/uni_modules/yao-RecordFrame/components/yao-RecordFrame/yao-RecordFrame.vue +++ b/src/uni_modules/yao-RecordFrame/components/yao-RecordFrame/yao-RecordFrame.vue @@ -2,6 +2,7 @@ +