326 lines
9.0 KiB
Plaintext
326 lines
9.0 KiB
Plaintext
<template>
|
|
<view class="max-div">
|
|
<cover-view class="pusher-container">
|
|
<view class="show-box" @click.stop>
|
|
<image :src="dataInfo.ossAddr" class="show-box-avatar"></image>
|
|
<view :style="{ height: iStatusBarHeight + 'px' }"></view>
|
|
<view class="switch-div">
|
|
<text class="title-time" v-if="isRecording">{{ formattedTime }}</text>
|
|
<text class="title-tip" v-if="!isRecording">正在接通中</text>
|
|
</view>
|
|
<view class="fl1"></view>
|
|
<view class="close-btn" @click="closeTalking">
|
|
<image src="/src/static/images/sparring/close-talk.png" mode="" class="close-btn-img"></image>
|
|
</view>
|
|
<talk-camera
|
|
ref="talkCameraRef"
|
|
></talk-camera>
|
|
</view>
|
|
</cover-view>
|
|
<talk-microphone ref="talkMicrophoneRef"></talk-microphone>
|
|
<talk-speaker ref="talkSpeakerRef" @playOver="playOver"></talk-speaker>
|
|
</view>
|
|
</template>
|
|
<script setup>
|
|
import { ref, reactive, onMounted, onUnmounted, computed, getCurrentInstance, nextTick, toRaw } from 'vue';
|
|
import { onShow, onLoad, onUnload, onBackPress, onReady } from '@dcloudio/uni-app';
|
|
import imagePreviewNvue from '@/components/image-preview/image-preview-nvue.vue';
|
|
import { get_base_url } from '@/api/request.js';
|
|
import { getToken } from '@/common/common.js';
|
|
import { ProtocolCodec, MessageType } from './js/ProtocolCodec';
|
|
import common from '@/common/common';
|
|
import talkMicrophone from './components/talk-microphone.vue';
|
|
import talkSpeaker from './components/talk-speaker.vue';
|
|
import talkCamera from './components/talk-camera';
|
|
import useWebSocket from './js/useWebSocket';
|
|
const instance = getCurrentInstance();
|
|
const componentProxy = instance.ctx;
|
|
const talkMicrophoneRef = ref(null);
|
|
const talkSpeakerRef = ref(null);
|
|
const talkCameraRef = ref(null);
|
|
|
|
const { initConnection, close, send, isRecording } = useWebSocket();
|
|
const iStatusBarHeight = ref(20);
|
|
const isVideo = ref(false);
|
|
const livePusherContext = ref(null);
|
|
|
|
const dataInfo = reactive({
|
|
traId: '',
|
|
chaId: '',
|
|
traType: '',
|
|
traInterTyp: '',
|
|
execId: '',
|
|
sceneDesc: '',
|
|
preview: false, // 是否是预览
|
|
traInterTypDesc: '', // 类型的文字提示,例如 语音通话
|
|
ossAddr: '', //用户头像
|
|
chaName: '' //用户名称
|
|
});
|
|
// 关闭通话
|
|
const closeTalking = async () => {
|
|
// todo 防抖
|
|
common.show('', '挂断视频将结束训练,是否确认?').then((res) => {
|
|
if (res) {
|
|
destroy(); // 走关闭销毁流程
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleTouchStart = () => {
|
|
isVideo.value && talkCameraRef.value.switchCamera();
|
|
};
|
|
// 关闭销毁流程,逐层清理
|
|
const destroy = (type = 0) => {
|
|
if (type <= 0) {
|
|
isVideo.value && talkCameraRef.value.stopPreview();
|
|
talkMicrophoneRef.value && talkMicrophoneRef.value.stop();
|
|
talkSpeakerRef.value && talkSpeakerRef.value.close();
|
|
}
|
|
if (type <= 1) {
|
|
close();
|
|
}
|
|
if (type <= 2) {
|
|
nextTick(() => {
|
|
setTimeout(() => {
|
|
if (dataInfo.execId) {
|
|
common.redirectTo(
|
|
'/pages/sparring/result?isOver=1&traId=' +
|
|
dataInfo.traId +
|
|
'&execId=' +
|
|
dataInfo.execId +
|
|
'&type=' +
|
|
dataInfo.traType +
|
|
'&isPreview=' +
|
|
(dataInfo.preview ? '1' : '')
|
|
);
|
|
} else {
|
|
common.navigateBack();
|
|
}
|
|
}, 300);
|
|
});
|
|
}
|
|
};
|
|
// 未接通时候报错处理,仅返回和少量销毁
|
|
const noConnectDestroy = () => {
|
|
console.log('未接通');
|
|
setTimeout(() => {
|
|
common.show('', '当前服务器繁忙,请稍后在试!', false).then((res) => {
|
|
if (res) common.navigateBack();
|
|
});
|
|
}, 500);
|
|
};
|
|
const onCurrentDecibels = (res) => {
|
|
console.log('当前分贝', res);
|
|
};
|
|
|
|
// 定时器实例
|
|
let timer = null;
|
|
|
|
// 2. 格式化函数:总秒数 → mm:ss(分钟无上限,不进位小时)
|
|
const formatTime = (totalSeconds) => {
|
|
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')}`; // 秒补零
|
|
};
|
|
|
|
const targetTimestamp = ref(0);
|
|
// 计算属性:实时格式化剩余时间
|
|
const formattedTime = ref('00:00');
|
|
// 3. 更新剩余时间的核心方法
|
|
const updateRemaining = () => {
|
|
const now = Date.now();
|
|
const diff = now - targetTimestamp.value;
|
|
const seconds = Math.max(0, Math.floor(diff / 1000));
|
|
formattedTime.value = formatTime(seconds);
|
|
};
|
|
|
|
const onStartRecord = (body) => {
|
|
dataInfo.execId = body.execId;
|
|
targetTimestamp.value = Date.now();
|
|
// 当ws连接成功时候
|
|
console.log('连接成功开始' + dataInfo.traInterTypDesc);
|
|
isRecording.value = true;
|
|
isVideo.value && talkCameraRef.value.startPreview();
|
|
if (timer) clearInterval(timer); // 清除旧的定时器,避免多个定时器叠加
|
|
timer = setInterval(updateRemaining, 400);
|
|
// 立即执行一次
|
|
updateRemaining();
|
|
talkMicrophoneRef.value &&
|
|
talkMicrophoneRef.value.start({
|
|
sampleRate: 16000,
|
|
frameSize: 1024,
|
|
gain: 1.0,
|
|
// 音频数据的帧接口回调,直接转码后接入到websocket的发送函数
|
|
onFrameRecorded: ({ isLastFrame, frameBuffer }) =>
|
|
send(ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer))
|
|
});
|
|
};
|
|
const playOver = (id) => {
|
|
console.log('播放完毕talk通知后端', id);
|
|
if (id) {
|
|
setTimeout(() => {
|
|
send(
|
|
ProtocolCodec.pack(MessageType.CONTROL_CMD, {
|
|
type: 2,
|
|
lock: id
|
|
})
|
|
);
|
|
}, 300);
|
|
}
|
|
};
|
|
const onError = (error) => {
|
|
// websocket错误处理
|
|
console.log('websocket错误处理', error.code);
|
|
common.show('', '系统或网络异常,通话结束!', false).then((res) => {
|
|
if (res) {
|
|
destroy();
|
|
}
|
|
});
|
|
};
|
|
const onClose = (error) => {
|
|
// websocket错误处理
|
|
console.log('websocketonClose关闭', error);
|
|
if (error?.code === 1002) {
|
|
// 在后端服务失败的时候连接不上
|
|
if (dataInfo.execId !== '') {
|
|
// 有execId说明通话过程中
|
|
destroy();
|
|
} else {
|
|
// 否则是未接通
|
|
noConnectDestroy();
|
|
}
|
|
} else if (error?.code === 1001) {
|
|
// 正常运行中后端掉线了
|
|
// onError({code:1001}); //处理逻辑和报错一样,直接调用报错方法
|
|
} else if (error?.code === 1011) {
|
|
// 服务器错误
|
|
onError({
|
|
code: 1011
|
|
}); //处理逻辑和报错一样,直接调用报错方法
|
|
} else if (error?.code === 1000) {
|
|
// 用户主动挂断
|
|
} else if (error?.code === 4001) {
|
|
// 自定义异常挂断,参数错误
|
|
noConnectDestroy();
|
|
}
|
|
};
|
|
onLoad((e) => {
|
|
dataInfo.traId = e.traId ?? '';
|
|
dataInfo.chaId = e.chaId ?? '';
|
|
dataInfo.traType = '02'; // 陪练类型 01-练习 02-考试
|
|
dataInfo.traInterTyp = e.traInterTyp ?? '02'; // 陪练交互类型 01 对话 02 语音 03 视频
|
|
// dataInfo.traInterType = '02'; // 陪练交互类型 01 对话 02 语音 03 视频
|
|
//#ifdef APP-PLUS
|
|
// 只有app才能打开摄像头
|
|
isVideo.value = dataInfo.traInterTyp === '03';
|
|
iStatusBarHeight.value = uni.getSystemInfoSync().statusBarHeight;
|
|
//#endif
|
|
dataInfo.preview = e.isPreview === '1'; // 是否是预览
|
|
const pageData = common.getPageCache('tipMsgsceneDesc');
|
|
// const pageData = {
|
|
// ossAddr: '/traoss/tras3/show/S3F0250181220722025120514463400000100717',
|
|
// chaName: '陈先生',
|
|
// traInterTyp: '03',
|
|
// traInterTypDesc: '视频通话'
|
|
// };
|
|
const base_url = get_base_url();
|
|
const token = getToken();
|
|
dataInfo.ossAddr = base_url + pageData.ossAddr + '?tk=' + token;
|
|
dataInfo.chaName = pageData.chaName;
|
|
dataInfo.traInterTypDesc = pageData.traInterTypDesc;
|
|
});
|
|
onMounted(() => {
|
|
initConnection({
|
|
authParams: {
|
|
traId: dataInfo.traId,
|
|
chaId: dataInfo.chaId,
|
|
traType: dataInfo.traType,
|
|
traInterTyp: dataInfo.traInterTyp,
|
|
preview: dataInfo.preview
|
|
},
|
|
onStartRecord: onStartRecord,
|
|
playPcmCallback: (res) => talkSpeakerRef.value.playPCM(res),
|
|
setPlayNumber: (id) => talkSpeakerRef.value.setPlayNumber(id),
|
|
onError,
|
|
onClose
|
|
});
|
|
});
|
|
|
|
onReady(() => {
|
|
if (isVideo.value) {
|
|
talkCameraRef.value.init(componentProxy);
|
|
}
|
|
});
|
|
onUnmounted(() => {
|
|
console.log('组件卸载');
|
|
if (timer) clearInterval(timer);
|
|
});
|
|
onBackPress((res) => {
|
|
if (res.from === 'backbutton') {
|
|
closeTalking();
|
|
return true;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.fl1 {
|
|
flex: 1;
|
|
}
|
|
.max-div {
|
|
width: 750rpx;
|
|
flex: 1;
|
|
position: relative;
|
|
display: flex;
|
|
}
|
|
.show-box-avatar {
|
|
z-index: 400;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
flex: 1;
|
|
display: flex;
|
|
}
|
|
|
|
.show-box {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
z-index: 999;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
background-color: rgba(0, 0, 0, 0.2);
|
|
flex: 1;
|
|
}
|
|
.title-time,
|
|
.title-tip {
|
|
color: #fff;
|
|
font-size: 38rpx;
|
|
}
|
|
.close-btn-img {
|
|
width: 118rpx;
|
|
height: 118rpx;
|
|
margin-top: 46px;
|
|
margin-bottom: 112px;
|
|
}
|
|
.pusher-container {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
z-index: 200;
|
|
flex: 1;
|
|
display: flex;
|
|
}
|
|
|
|
</style>
|