修复语音通话前后端播放标识

This commit is contained in:
田岩
2025-12-09 13:26:19 +08:00
parent 00d313c12f
commit c15375aac1
7 changed files with 159 additions and 71 deletions
+2 -2
View File
@@ -6,7 +6,7 @@ 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'
@@ -17,7 +17,7 @@ ENV = 'development'
#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
@@ -105,7 +105,7 @@
if (options.gain > 20.0) {
options.gain = 20.0;
}
// #ifdef APP-PLUS
const os = plus.os.name;
if (os === 'Android') {
console.log('是安卓');
@@ -136,7 +136,12 @@
audioContext = new AudioContext();
await audioContext.audioWorklet.addModule('static/js/processor.worklet.js');
}
//#endif
//#ifdef H5
audioContext = new AudioContext();
await audioContext.audioWorklet.addModule('static/js/processor.worklet.js');
//#endif
mediaStream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: options.sampleRate,
+70 -28
View File
@@ -12,31 +12,72 @@
></view>
</template>
<script setup>
import { ref, onUnmounted, watch, onMounted } from 'vue';
const isStop = ref(false);
const currentPCMChunk = ref(false);
const audioOutput = ref('earpiece');
// 响应式状态
const isPlaying = ref(false); // 是否正在播放
const volume = ref(1); // 播放音量(0~1
const playPCM = (pcmData) => {
if (!pcmData || pcmData.byteLength === 0) return;
currentPCMChunk.value = pcmData;
};
const close = () => {
isStop.value = false;
};
const changeStreamPlaying = () => {
console.log('播放结束');
};
const switchOutput = (type) => {
if (['speaker', 'earpiece'].includes(type)) {
audioOutput.value = type;
<script>
export default {
// 对应组合式API的响应式数据
data() {
return {
isStop: false,
currentPCMChunk: false,
playNumber: '',
audioOutput: 'earpiece',
isPlaying: false, // 是否正在播放
volume: 1 // 播放音量(0~1
};
},
// 生命周期钩子对应
mounted() {
// 原onMounted逻辑(此处无代码,保留空钩子)
},
unmounted() {
// 原onUnmounted逻辑(此处无代码,保留空钩子)
},
// 对应组合式API的方法
methods: {
// 播放PCM数据
playPCM(pcmData) {
if (!pcmData || pcmData.byteLength === 0) return;
this.currentPCMChunk = pcmData;
},
// 设置播放id
setPlayNumber(id) {
console.log('组件内设置播放', id);
this.playNumber = id;
},
// 关闭/重置停止状态
close() {
this.isStop = false;
},
// 播放状态变更
changeStreamPlaying({ status }) {
if (status === false) {
console.log('播放结束', this.playNumber);
this.$emit('playOver', this.playNumber)
}
},
// 切换音频输出方式
switchOutput(type) {
if (['speaker', 'earpiece'].includes(type)) {
this.audioOutput = type;
}
}
},
// 若需要watch监听(原组合式API的watch),补充如下(当前代码无watch逻辑,可删)
watch: {
// 示例:监听playNumber变化(如有需要)
// playNumber(newVal) {
// console.log('播放id变更:', newVal);
// }
}
};
defineExpose({ playPCM, close, switchOutput });
</script>
<script module="renderJS" lang="renderjs">
@@ -67,7 +108,7 @@
}
}
} catch (e) {}
// 2. 安卓 H5 特殊处理:通过 Audio 对象的 speakerphone 属性(部分浏览器支持)
const audio = new Audio();
if (audio.setSpeakerphoneOn) {
@@ -76,7 +117,7 @@
audio.play(); // 触发设置生效
audio.pause(); // 无需播放实际音频
}
// 3. 小程序端(如微信小程序):调用小程序原生 API
if (wx && wx.setAudioRoute) {
// 微信小程序专属:切换音频输出(需基础库 2.10.0+)
@@ -86,11 +127,11 @@
fail: (e) => console.error('小程序路由切换失败:', e)
});
}
// 4. 兜底提示(非 5+ 环境)
console.log(`当前环境不支持强制音频路由,已模拟${type === 'earpiece' ? '听筒' : '扬声器'}效果`);
}
export default {
mounted() {
const _this = this;
@@ -135,8 +176,9 @@
},
// 播放结束回调(通知主线程)
callback(e) {
console.log('播放结束回调', e);
if (e === 'ended') {
this.$ownerInstance.callMethod('changeStreamPlaying', { type: false });
this.$ownerInstance.callMethod('changeStreamPlaying', {status:false});
}
}
}
+30 -5
View File
@@ -31,7 +31,13 @@ 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 initConnection = ({
authParams = {},
onStartRecord,
playPcmCallback,
setPlayNumber = () => {}, //设置当前播放的音频的id,用于播放完成回调
onError = () => {}
}) => {
const wsUrl = _baseUrl + url + '?summary=' + getToken()
const finalAuthParams = {
...DEFAULT_AUTH_PARAMS,
@@ -57,6 +63,7 @@ export default function useWebSocket(aaas = null, options = {}) {
body
} = ProtocolCodec.unpack(res.data);
switch (msgType) {
case MessageType.IDENTITY:
console.log('身份校验', body);
onStartRecord(body)
@@ -66,16 +73,34 @@ export default function useWebSocket(aaas = null, options = {}) {
if (buffer.byteLength < 2) break;
const uint8 = new Uint8Array(buffer);
const numArray = Array.from(uint8);
// currentPCMChunk.value = numArray;
sssssss(numArray)
// console.log('音频消息', body);
playPcmCallback(numArray)
break;
case MessageType.ERROR:
switch (body.errCode) {
case 1:
// ASR或TTS初始化出错
case 4:
// 通话接通失败,请稍后再试!
break;
default:
// 其他错误码的默认处理(如果需要)
break;
}
console.log('错误', body);
// this.handleErrorMessage(body);
break;
case MessageType.CONTROL_CMD:
// 控制消息
console.log('控制消息', msgType, body);
if (body.type === 1) { // 开始播放,记录当前播放的语音id
setPlayNumber(body.lock)
}
break;
default:
console.log('其他消息', body);
console.log('其他消息', msgType, body);
break;
}
});
+50 -34
View File
@@ -4,8 +4,8 @@
<view class="show-box">
<nav-bar :is_seat="true"></nav-bar>
<view class="switch-div">
<view class="switch-text">场景提示</view>
<switch class="switch" :checked="!sceneDescShow" @click="switchChange" />
<!-- <view class="switch-text">场景提示</view> -->
<!-- <switch class="switch" :checked="!sceneDescShow" @click="switchChange" /> -->
</view>
<view class="fl1-div-1"></view>
<view class="role-info">
@@ -21,11 +21,11 @@
<view class="close-btn" @click="closeTalking"></view>
</view>
<talk-microphone ref="talkMicrophoneRef"></talk-microphone>
<talk-speaker ref="talkSpeakerRef"></talk-speaker>
<talk-speaker ref="talkSpeakerRef" @playOver="playOver"></talk-speaker>
</view>
</template>
<script setup>
import { getUserInfo } from '@/common/common';
import { getUserInfo,getToken } from '@/common/common';
import { queryTraPartnerCharacterInfoById, partnerChatReportTrigger } from '@/api/sparring.js';
import common from '@/common/common';
import talkMicrophone from './components/talk-microphone.vue';
@@ -47,7 +47,6 @@
ControlCommand,
ProtocolConst
} from './js/ProtocolCodec';
import { getToken } from '@/common/common';
const dataInfo = reactive({
traId: '',
chaId: '',
@@ -63,32 +62,34 @@
// 关闭通话
const closeTalking = async () => {
talkMicrophoneRef.value.stop();
talkSpeakerRef.value.close();
close();
common.navigateBack(1, () => {
partnerChatReportTrigger({
execId: dataInfo.execId,
traId: dataInfo.traId,
talkingType: dataInfo.isPreview ? 'preview' : 'practice'
}).then((res) => {
common.redirectTo(
'/pages/sparring/result?isOver=1&traId=' +
dataInfo.traId +
'&execId=' +
dataInfo.execId +
'&type=' +
dataInfo.traType +
'&isPreview=' +
(dataInfo.isPreview ? '1' : '')
);
});
common.show('', '挂断电话将结束训练,是否确认?').then((res) => {
if (res) {
talkMicrophoneRef.value.stop();
talkSpeakerRef.value.close();
close();
common.navigateBack(1, () => {
partnerChatReportTrigger({
execId: dataInfo.execId,
traId: dataInfo.traId,
talkingType: dataInfo.isPreview ? 'preview' : 'practice'
}).then((res) => {
common.redirectTo(
'/pages/sparring/result?isOver=1&traId=' +
dataInfo.traId +
'&execId=' +
dataInfo.execId +
'&type=' +
dataInfo.traType +
'&isPreview=' +
(dataInfo.isPreview ? '1' : '')
);
});
});
}
});
};
function frameRecorded(res) {
// console.log('res', res);
send(res);
}
const onCurrentDecibels = (res) => {
console.log('res', res);
@@ -101,8 +102,8 @@
// 2. 格式化函数:总秒数 → mm:ss(分钟无上限,不进位小时)
const formatTime = (totalSeconds) => {
if (totalSeconds <= 0) return '0:00'; // 过期显示0:00
const minutes = Math.floor(totalSeconds / 60); // 分钟无上限
if (totalSeconds <= 0) return '00:00'; // 过期显示0:00
const minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0'); // 分钟无上限
const seconds = totalSeconds % 60; // 秒数
return `${minutes}:${seconds.toString().padStart(2, '0')}`; // 秒补零
};
@@ -115,7 +116,7 @@
const diff = now - targetTimestamp.value;
formattedTime.value = formatTime(Math.max(0, Math.floor(diff / 1000)));
};
const onStartRecord = (body) => {
dataInfo.execId = body.execId;
targetTimestamp.value = Date.now();
@@ -125,7 +126,6 @@
timer = setInterval(updateRemaining, 1000);
// 立即执行一次
updateRemaining();
talkMicrophoneRef.value.start({
sampleRate: 16000,
frameSize: 1024,
@@ -134,6 +134,16 @@
onFrameRecorded: ({ isLastFrame, frameBuffer }) => send(ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer))
});
};
const playOver = (id) => {
send(ProtocolCodec.pack(MessageType.CONTROL_CMD, {
type:2,
lock:id
}))
};
const onError = (body) => {
// websocket错误处理
};
onLoad((e) => {
dataInfo.traId = e.traId ?? '';
dataInfo.chaId = e.chaId ?? '';
@@ -147,7 +157,13 @@
});
onMounted(() => {
initConnection(dataInfo, onStartRecord, (res) => talkSpeakerRef.value.playPCM(res));
initConnection({
authParams:Object.assign({}, dataInfo),
onStartRecord:onStartRecord,
playPcmCallback:(res) => talkSpeakerRef.value.playPCM(res),
setPlayNumber:(id) => talkSpeakerRef.value.setPlayNumber(id),
onError:onError
});
});
onUnmounted(() => {
console.log('组件卸载');