Files
tra-app/src/pages/sparring/talkVideo.nvue
T
2025-12-17 10:22:32 +08:00

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>