Merge branch 'develop' of http://25.13.9.101:9000/K17_AITS/tra-app into develop
This commit is contained in:
@@ -12,11 +12,8 @@
|
||||
:whiteness="0"
|
||||
aspect="9:16"
|
||||
mode="SD"
|
||||
device-position="front"
|
||||
orientation
|
||||
></live-pusher>
|
||||
</cover-view>
|
||||
<!-- <cover-view class="corner-mask"></cover-view> -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -27,9 +24,8 @@
|
||||
const livePusherContext = ref(null);
|
||||
const isPreviewing = ref(false);
|
||||
const init = (componentProxy) => {
|
||||
console.log('init');
|
||||
|
||||
livePusherContext.value = uni.createLivePusherContext('livePusher', componentProxy);
|
||||
livePusherContext.value.switchCamera()
|
||||
};
|
||||
// 切换摄像头
|
||||
const switchCamera = () => {
|
||||
@@ -59,13 +55,10 @@
|
||||
console.error('complete预览:', err);
|
||||
}
|
||||
});
|
||||
|
||||
if (getPhoneEnvBool('AND')) {
|
||||
console.log('切换前置');
|
||||
// setTimeout(() =>{
|
||||
// switchCamera()
|
||||
// }, 1000)
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
@@ -83,7 +76,6 @@
|
||||
console.error('complete预览:', err);
|
||||
}
|
||||
});
|
||||
|
||||
isPreviewing.value = false;
|
||||
};
|
||||
defineExpose({ init, switchCamera, stopPreview, startPreview });
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
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
|
||||
};
|
||||
};
|
||||
@@ -1,129 +0,0 @@
|
||||
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 // 手动清理资源
|
||||
};
|
||||
};
|
||||
+67
-49
@@ -1,11 +1,13 @@
|
||||
<template>
|
||||
<view>
|
||||
<image-preview :src="dataInfo.ossAddr" class="show-box-avatar"></image-preview>
|
||||
<view class="show-box">
|
||||
<view class="show-box" :class="{ isVideo: isVideoStyle }">
|
||||
<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="title-time" v-if="isRecording">{{ formattedTime }}</view>
|
||||
<view class="title-tip" v-if="!isRecording">正在接通中</view>
|
||||
</view>
|
||||
<view class="fl1-div-1"></view>
|
||||
<view class="role-info">
|
||||
@@ -34,13 +36,13 @@
|
||||
import talkSpeaker from './components/talk-speaker.vue';
|
||||
import talkCamera from './components/talk-camera';
|
||||
import useWebSocket from './js/useWebSocket';
|
||||
import useMicrophone from './js/useMicrophone';
|
||||
const talkMicrophoneRef = ref(null);
|
||||
const talkSpeakerRef = ref(null);
|
||||
const talkCameraRef = ref(null);
|
||||
|
||||
const { initConnection, close, send, isRecording } = useWebSocket();
|
||||
const isVideo = ref(false);
|
||||
const isVideoStyle = computed(() => dataInfo.traInterTyp === '03');
|
||||
|
||||
const dataInfo = reactive({
|
||||
traId: '',
|
||||
@@ -62,7 +64,7 @@
|
||||
// 关闭通话
|
||||
const closeTalking = async () => {
|
||||
// todo 防抖
|
||||
common.show('', '挂断电话将结束训练,是否确认?').then((res) => {
|
||||
common.show('', '挂断'+(isVideoStyle.value?'视频':'电话')+'将结束训练,是否确认?').then((res) => {
|
||||
if (res) {
|
||||
destroy(); // 走关闭销毁流程
|
||||
}
|
||||
@@ -70,7 +72,7 @@
|
||||
};
|
||||
const test = () => {
|
||||
isVideo.value && talkCameraRef.value.switchCamera();
|
||||
}
|
||||
};
|
||||
// 关闭销毁流程,逐层清理
|
||||
const destroy = (type = 0) => {
|
||||
if (type <= 0) {
|
||||
@@ -184,19 +186,19 @@
|
||||
destroy();
|
||||
} else {
|
||||
// 否则是未接通
|
||||
noConnectDestroy()
|
||||
noConnectDestroy();
|
||||
}
|
||||
} else if (error?.code === 1001) {
|
||||
// 正常运行中后端掉线了
|
||||
// onError({code:1001}); //处理逻辑和报错一样,直接调用报错方法
|
||||
} else if (error?.code === 1011) {
|
||||
// 服务器错误
|
||||
onError({code:1011}); //处理逻辑和报错一样,直接调用报错方法
|
||||
onError({ code: 1011 }); //处理逻辑和报错一样,直接调用报错方法
|
||||
} else if (error?.code === 1000) {
|
||||
// 用户主动挂断
|
||||
} else if (error?.code === 4001) {
|
||||
// 自定义异常挂断,参数错误
|
||||
noConnectDestroy()
|
||||
noConnectDestroy();
|
||||
}
|
||||
};
|
||||
onLoad((e) => {
|
||||
@@ -243,6 +245,61 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.isVideo {
|
||||
// background-color: red;
|
||||
.role-info {
|
||||
display: none;
|
||||
}
|
||||
&.show-box {
|
||||
background: linear-gradient(
|
||||
120deg,
|
||||
rgba(51, 51, 51, 0.2) 0%,
|
||||
rgba(102, 102, 102, 0.2) 50%,
|
||||
rgba(51, 51, 51, 0.2) 100%
|
||||
);
|
||||
backdrop-filter: blur(0px);
|
||||
}
|
||||
}
|
||||
|
||||
.title-time,
|
||||
.title-tip {
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
line-height: 58rpx;
|
||||
font-size: 38rpx;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.role-name {
|
||||
margin-bottom: 26rpx;
|
||||
}
|
||||
|
||||
.title-tip {
|
||||
padding-right: 20rpx;
|
||||
position: relative;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
animation: loading 2.5s infinite;
|
||||
}
|
||||
@keyframes loading {
|
||||
0% {
|
||||
content: '';
|
||||
}
|
||||
25% {
|
||||
content: '.';
|
||||
}
|
||||
50% {
|
||||
content: '..';
|
||||
}
|
||||
75% {
|
||||
content: '...';
|
||||
}
|
||||
100% {
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.switch-div {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -250,6 +307,7 @@
|
||||
margin-right: 20rpx;
|
||||
margin-top: 10rpx;
|
||||
width: 100%;
|
||||
|
||||
.switch-text {
|
||||
font-weight: 400;
|
||||
font-size: 28rpx;
|
||||
@@ -285,10 +343,10 @@
|
||||
max-height: 240rpx;
|
||||
}
|
||||
.show-box {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(120deg, #333333cc 0%, #666666cc 50%, #333333cc 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -311,10 +369,6 @@
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
padding-top: 210rpx;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
line-height: 58rpx;
|
||||
font-size: 38rpx;
|
||||
|
||||
.role-avatar {
|
||||
width: 185rpx;
|
||||
@@ -326,42 +380,6 @@
|
||||
transform: translateX(-50%);
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.role-name {
|
||||
margin-bottom: 26rpx;
|
||||
}
|
||||
|
||||
.title-tip {
|
||||
padding-right: 20rpx;
|
||||
position: relative;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
animation: loading 2.5s infinite;
|
||||
}
|
||||
@keyframes loading {
|
||||
0% {
|
||||
content: '';
|
||||
}
|
||||
25% {
|
||||
content: '.';
|
||||
}
|
||||
50% {
|
||||
content: '..';
|
||||
}
|
||||
75% {
|
||||
content: '...';
|
||||
}
|
||||
100% {
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
.title-tip {
|
||||
&:not(.connecting) {
|
||||
// display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body-scene {
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
<template>
|
||||
<view>
|
||||
<video :is-live="true" id="myVideo" :src="videoSrc" :autoplay="true"></video>
|
||||
<live-pusher id="livePusher" ref="livePusher" class="livePusher" url="rtmp://25.18.122.148:9605/voice_in/aa" mode="SD" :muted="true" :enable-camera="false" :auto-focus="true" :beauty="1" whiteness="2" aspect="9:16" @statechange="statechange" @netstatus="netstatus" @error="error"></live-pusher>
|
||||
|
||||
<view class="btn" @click="prepare"><text>预热</text></view>
|
||||
<view class="btn" @click="start"><text>开始推流</text></view>
|
||||
<view class="btn" @click="pause"><text>暂停推流</text></view>
|
||||
<view class="btn" @click="resume"><text>恢复推流</text></view>
|
||||
<view class="btn" @click="stop"><text>停止推流</text></view>
|
||||
<view class="btn" @click="snapshot"><text>快照</text></view>
|
||||
<view class="btn" @click="startPreview"><text>开启摄像头预览</text></view>
|
||||
<view class="btn" @click="stopPreview"><text>关闭摄像头预览</text></view>
|
||||
<view class="btn" @click="switchCamera"><text>切换摄像头</text></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { prepareApi } from '@/api/talk.js'
|
||||
var pusher
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
context: null,
|
||||
baseName: 'zhangsan'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
videoSrc() {
|
||||
return 'rtmp://25.18.122.148:9605/voice_out/' + this.baseName
|
||||
}
|
||||
},
|
||||
onReady() {
|
||||
const videoContext = uni.createVideoContext('myVideo')
|
||||
videoContext.setVolume(1.0)
|
||||
// 注意:需要在onReady中 或 onLoad 延时
|
||||
// pusher = uni.createLivePusherContext('livePusher', this)
|
||||
// console.log(pusher)
|
||||
},
|
||||
methods: {
|
||||
statechange(e) {
|
||||
console.log('statechange:', e, JSON.stringify(e))
|
||||
},
|
||||
netstatus(e) {
|
||||
console.log('netstatus:' + JSON.stringify(e))
|
||||
},
|
||||
error(e) {
|
||||
console.log('error:' + JSON.stringify(e))
|
||||
},
|
||||
start: function () {
|
||||
console.log('statechange:', pusher.start)
|
||||
pusher.start({
|
||||
success: a => {
|
||||
console.log('livePusher.start:' + JSON.stringify(a))
|
||||
}
|
||||
})
|
||||
},
|
||||
prepare: function () {
|
||||
prepareApi(this.baseName)
|
||||
.then(res => {
|
||||
console.log(res)
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
if (err === 'prepare') {
|
||||
this.initPusher()
|
||||
}
|
||||
})
|
||||
},
|
||||
close: function () {
|
||||
pusher.close({
|
||||
success: a => {
|
||||
console.log('livePusher.close:' + JSON.stringify(a))
|
||||
}
|
||||
})
|
||||
},
|
||||
snapshot: function () {
|
||||
pusher.snapshot({
|
||||
success: e => {
|
||||
console.log(JSON.stringify(e))
|
||||
}
|
||||
})
|
||||
},
|
||||
resume: function () {
|
||||
pusher.resume({
|
||||
success: a => {
|
||||
console.log('livePusher.resume:' + JSON.stringify(a))
|
||||
}
|
||||
})
|
||||
},
|
||||
pause: function () {
|
||||
pusher.pause({
|
||||
success: a => {
|
||||
console.log('livePusher.pause:' + JSON.stringify(a))
|
||||
}
|
||||
})
|
||||
},
|
||||
stop: function () {
|
||||
pusher.stop({
|
||||
success: a => {
|
||||
console.log(JSON.stringify(a))
|
||||
}
|
||||
})
|
||||
},
|
||||
switchCamera: function () {
|
||||
pusher.switchCamera({
|
||||
success: a => {
|
||||
console.log('livePusher.switchCamera:' + JSON.stringify(a))
|
||||
}
|
||||
})
|
||||
},
|
||||
startPreview: function () {
|
||||
pusher.startPreview({
|
||||
success: a => {
|
||||
console.log('livePusher.startPreview:' + JSON.stringify(a))
|
||||
}
|
||||
})
|
||||
},
|
||||
stopPreview: function () {
|
||||
pusher.stopPreview({
|
||||
success: a => {
|
||||
console.log('livePusher.stopPreview:' + JSON.stringify(a))
|
||||
}
|
||||
})
|
||||
},
|
||||
initPusher() {
|
||||
console.log(1)
|
||||
pusher = new plus.video.LivePusher('pusher-box', {
|
||||
url: 'rtmp://25.18.122.148:9605/voice_in/' + this.baseName,
|
||||
'enable-camera': false
|
||||
})
|
||||
pusher.addEventListener('statechange', this.statechange)
|
||||
pusher.addEventListener('netstatus', this.netstatus)
|
||||
pusher.addEventListener('error', this.error)
|
||||
console.log(pusher)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.btn {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background-color: #ccc;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,182 +0,0 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<button @click="startPush" v-if="!isPushing">开始语音推流</button>
|
||||
<button @click="stopPush" v-else>停止语音推流</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isPushing: false, // 推流状态
|
||||
pusher: null, // 推流实例
|
||||
currentWebview: null // 当前 Webview 实例
|
||||
}
|
||||
},
|
||||
onReady() {
|
||||
// 改用 onReady(页面渲染完成) + 延迟
|
||||
if (uni.getSystemInfoSync().platform === 'android') {
|
||||
// 安卓端延迟 1000ms,且重试 3 次,确保获取到 Webview
|
||||
let retryCount = 0
|
||||
const getWebview = () => {
|
||||
this.currentWebview = this.$mp.page.$getAppWebview()
|
||||
if (this.currentWebview) {
|
||||
console.log('Webview 获取成功:', this.currentWebview)
|
||||
return
|
||||
}
|
||||
retryCount++
|
||||
if (retryCount < 3) {
|
||||
setTimeout(getWebview, 500) // 每次重试间隔 500ms
|
||||
} else {
|
||||
uni.showToast({ title: 'Webview 初始化失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
setTimeout(getWebview, 1000) // 初始延迟 1 秒
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
// // 仅 App 端初始化 Webview
|
||||
// if (uni.getSystemInfoSync().platform !== 'app-plus') {
|
||||
// uni.showToast({ title: '仅支持 App 端推流', icon: 'none' });
|
||||
// return;
|
||||
// }
|
||||
// this.currentWebview = this.$mp.page.$getAppWebview();
|
||||
},
|
||||
onUnload() {
|
||||
// 页面销毁时停止推流并销毁组件
|
||||
this.stopPush()
|
||||
},
|
||||
methods: {
|
||||
// 申请麦克风权限
|
||||
async requestMicPermission() {
|
||||
return new Promise((resolve, reject) => {
|
||||
plus.android.requestPermissions(
|
||||
['android.permission.RECORD_AUDIO'], // Android 权限
|
||||
res => {
|
||||
const granted = res.filter(item => item.granted).length === 1
|
||||
granted ? resolve() : reject('麦克风权限申请失败')
|
||||
},
|
||||
err => reject(err)
|
||||
)
|
||||
// iOS 权限由系统自动弹窗,无需手动申请,此处可统一处理
|
||||
})
|
||||
},
|
||||
async startPush() {
|
||||
if (uni.getSystemInfoSync().platform !== 'app-plus') return
|
||||
|
||||
try {
|
||||
await this.requestMicPermission()
|
||||
|
||||
// 优先用原生 API 获取 Webview(兜底)
|
||||
this.currentWebview = plus.webview.currentWebview() || this.$mp.page.$getAppWebview()
|
||||
if (!this.currentWebview) {
|
||||
throw new Error('无法获取页面 Webview 实例')
|
||||
}
|
||||
|
||||
// 后续创建 pusher + append 逻辑
|
||||
this.pusher = plus.video.createLivePusher('', {
|
||||
/* 配置 */
|
||||
})
|
||||
this.currentWebview.append(this.pusher) // 此时不会再为 null
|
||||
// ... 启动推流
|
||||
} catch (err) {
|
||||
console.error('推流启动失败:', err)
|
||||
}
|
||||
},
|
||||
// 开始推流
|
||||
async startPush2() {
|
||||
console.log('11', uni.getSystemInfoSync().platform)
|
||||
// 1. 端判断
|
||||
if (uni.getSystemInfoSync().platform !== 'android') return
|
||||
console.log('22')
|
||||
try {
|
||||
// 2. 申请麦克风权限
|
||||
// await this.requestMicPermission();
|
||||
console.log('33')
|
||||
// 3. 创建 LivePusher 实例(仅音频,隐藏组件)
|
||||
this.pusher = plus.video.createLivePusher('', {
|
||||
url: 'rtmp://25.18.122.148:9605/voice_in/aa', // 你的 RTMP 推流地址
|
||||
top: '0px',
|
||||
left: '0px',
|
||||
width: '1px', // 隐藏组件(仅音频无需显示)
|
||||
height: '1px',
|
||||
position: 'static',
|
||||
// 核心:仅音频配置
|
||||
video: {
|
||||
enable: false // 关闭视频采集
|
||||
},
|
||||
audio: {
|
||||
enable: true, // 开启音频采集
|
||||
sampleRate: 16000, // 采样率(AI 语音识别常用 16k)
|
||||
bitrate: 64000, // 音频码率
|
||||
codec: 'aac' // 编码格式(RTMP 主流)
|
||||
},
|
||||
// 推流模式:仅音频直播
|
||||
mode: 'rtmp',
|
||||
autopush: false // 不自动推流,手动控制
|
||||
})
|
||||
console.log('44')
|
||||
// 4. 挂载到 Webview
|
||||
this.currentWebview.append(this.pusher)
|
||||
|
||||
// 5. 监听推流状态
|
||||
this.pusher.addEventListener('statechange', e => {
|
||||
console.log('推流状态变化:', e)
|
||||
switch (e.state) {
|
||||
case 'connecting':
|
||||
uni.showToast({ title: '正在连接推流服务器', icon: 'none' })
|
||||
break
|
||||
case 'connected':
|
||||
this.isPushing = true
|
||||
uni.showToast({ title: '推流成功', icon: 'success' })
|
||||
break
|
||||
case 'disconnected':
|
||||
this.isPushing = false
|
||||
uni.showToast({ title: '推流断开', icon: 'none' })
|
||||
break
|
||||
case 'error':
|
||||
this.isPushing = false
|
||||
uni.showToast({ title: `推流错误:${e.error}`, icon: 'none' })
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// 6. 启动预览 + 推流
|
||||
this.pusher.startPreview() // 启动音视频采集(必须调用)
|
||||
this.pusher.start() // 开始推流
|
||||
} catch (err) {
|
||||
uni.showToast({ title: err, icon: 'none' })
|
||||
console.error('推流启动失败:', err)
|
||||
}
|
||||
},
|
||||
|
||||
// 停止推流
|
||||
stopPush() {
|
||||
if (!this.pusher) return
|
||||
// 停止推流 + 预览
|
||||
this.pusher.stop()
|
||||
this.pusher.stopPreview()
|
||||
// 销毁组件
|
||||
this.pusher.close()
|
||||
this.pusher = null
|
||||
this.isPushing = false
|
||||
uni.showToast({ title: '推流已停止', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 20px;
|
||||
}
|
||||
button {
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
background: #007aff;
|
||||
color: #fff;
|
||||
border-radius: 10rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,492 +0,0 @@
|
||||
<template>
|
||||
<view>
|
||||
<cached-avatar class="show-box-avatar"></cached-avatar>
|
||||
<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>
|
||||
<view class="fl1-div-1"></view>
|
||||
<view class="role-info">
|
||||
<cached-avatar class="role-avatar"></cached-avatar>
|
||||
<view class="role-name">{{ userInfo.userName }}</view>
|
||||
<view class="title-time" v-if="isConnecting">{{ timeShow }}</view>
|
||||
<view class="title-tip" v-if="!isConnecting">正在接通中</view>
|
||||
</view>
|
||||
<view class="fl1"></view>
|
||||
<view class="body-scene" :class="{ show: sceneDescShow }">
|
||||
{{ sceneDesc }}
|
||||
</view>
|
||||
<view class="close-btn" @click="closeTalking"></view>
|
||||
</view>
|
||||
<yao-RecordFrame
|
||||
ref="recordFrame"
|
||||
@onFrameRecorded="frameRecorded"
|
||||
@currentDecibels="onCurrentDecibels"
|
||||
@onStop="stopIt"
|
||||
></yao-RecordFrame>
|
||||
<view
|
||||
ref="renderJSModule"
|
||||
:pcmChunk="currentPCMChunk"
|
||||
:change:pcmChunk="renderJS.appendPCMChunk"
|
||||
:isStop="isStop"
|
||||
:change:isStop="renderJS.stop"
|
||||
type="renderjs"
|
||||
module="renderJS"
|
||||
></view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import { getUserInfo } from '@/common/common';
|
||||
import talkCom from './talkCom.nvue';
|
||||
import { queryTraPartnerCharacterInfoById, partnerChatReportTrigger } from '@/api/sparring.js';
|
||||
import common from '@/common/common';
|
||||
import useWebSocket from './js/useWebSocket';
|
||||
const { initConnection, ws } = useWebSocket();
|
||||
import {
|
||||
ProtocolCodec,
|
||||
MessageType,
|
||||
SerializationType,
|
||||
CompressionType,
|
||||
ControlCommand,
|
||||
ProtocolConst
|
||||
} from './js/ProtocolCodec';
|
||||
import { getToken } from '@/common/common';
|
||||
export default {
|
||||
components: {
|
||||
talkCom // 注册组件
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
traId: '',
|
||||
chaId: '',
|
||||
talkingType: '',
|
||||
execId: '',
|
||||
isPreview: '',
|
||||
chaName: '',
|
||||
sceneDesc: '场景提示文案文儿提示文',
|
||||
sceneDescShow: false,
|
||||
timeShow: '11:19',
|
||||
isConnecting: false, // 是否接通中
|
||||
userInfo: {
|
||||
userName: ''
|
||||
},
|
||||
ws: null,
|
||||
wsUrl: 'wss://aitstest.jlbank.com.cn:7001/trapractice/voiceCallSocket/voiceCall?summary=' + getToken(), // 替换为实际后端地址,
|
||||
// 播放组件
|
||||
currentPCMChunk: null,
|
||||
isStop: false // 停止播放指令
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
callPhone() {
|
||||
// 拨打电话
|
||||
this.ws = uni.connectSocket({
|
||||
url: this.wsUrl,
|
||||
fail: () => {
|
||||
console.log('fail');
|
||||
},
|
||||
success: () => {
|
||||
console.log('web');
|
||||
},
|
||||
fail: () => {
|
||||
console.log('fail');
|
||||
}
|
||||
});
|
||||
this.ws.onOpen((res) => {
|
||||
console.log('WebSocket连接已打开type', this.type);
|
||||
this.ws.send({
|
||||
data: ProtocolCodec.pack(MessageType.IDENTITY, {
|
||||
traId: this.traId,
|
||||
chaId: this.chaId,
|
||||
type: this.type,
|
||||
user_id: '1001',
|
||||
token: 'your_auth_token',
|
||||
name: '测试用户'
|
||||
})
|
||||
});
|
||||
});
|
||||
this.ws.onMessage((res) => {
|
||||
const { msgType, body } = ProtocolCodec.unpack(res.data);
|
||||
switch (msgType) {
|
||||
case MessageType.IDENTITY:
|
||||
console.log('身份校验成功', body);
|
||||
this.onStartRecord();
|
||||
this.isConnecting = true;
|
||||
break;
|
||||
case MessageType.AUDIO_DATA:
|
||||
const buffer = body;
|
||||
if (buffer.byteLength < 2) break;
|
||||
const uint8 = new Uint8Array(buffer);
|
||||
const numArray = Array.from(uint8);
|
||||
this.currentPCMChunk = numArray;
|
||||
break;
|
||||
|
||||
case MessageType.ERROR:
|
||||
this.handleErrorMessage(body);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('其他消息', body);
|
||||
break;
|
||||
}
|
||||
});
|
||||
},
|
||||
handleErrorMessage(body) {
|
||||
// 独立的错误处理函数
|
||||
console.log(body);
|
||||
},
|
||||
disconnect() {
|
||||
// 断开连接时候清理
|
||||
// 清理音频相关资源
|
||||
// this.onStopRecord();
|
||||
// 重置状态
|
||||
// this.currentPCMChunk = null;
|
||||
// this.isConnecting = false;
|
||||
},
|
||||
manualDisconnect() {
|
||||
// 主动挂断
|
||||
// 标记主动挂断,跳过重连
|
||||
this.isManualDisconnect = true;
|
||||
this.disconnect();
|
||||
this.handleFinalDisconnect();
|
||||
this.isManualDisconnect = false;
|
||||
},
|
||||
startHeartbeat() {
|
||||
// 先清除旧心跳
|
||||
clearInterval(this.heartbeatTimer);
|
||||
// 定时发送心跳包,检测后端是否存活
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
if (!this.isConnected) return;
|
||||
// 发送心跳消息(根据你的协议定义,如MessageType.HEARTBEAT)
|
||||
this.sendMsg({
|
||||
type: MessageType.HEARTBEAT,
|
||||
body: { timestamp: Date.now() }
|
||||
});
|
||||
}, this.heartbeatInterval);
|
||||
},
|
||||
onStartRecord() {
|
||||
console.log('开始录音');
|
||||
try {
|
||||
this.$refs.recordFrame.start({
|
||||
sampleRate: 16000,
|
||||
frameSize: 1024,
|
||||
gain: 1.0,
|
||||
onFrameRecorded: ({ isLastFrame, frameBuffer }) => {
|
||||
// console.log('录音返回', frameBuffer);
|
||||
try {
|
||||
ws.value.send({
|
||||
data: ProtocolCodec.pack(MessageType.AUDIO_DATA, frameBuffer)
|
||||
});
|
||||
} catch (e) {}
|
||||
},
|
||||
onDecibels: (decibels) => {
|
||||
this.currentDecibels = decibels;
|
||||
}
|
||||
});
|
||||
this.isRecording = true;
|
||||
this.status = '录音中...';
|
||||
} catch (e) {
|
||||
console.error('启动录音失败:', e);
|
||||
this.status = '启动录音失败';
|
||||
this.isRecording = false;
|
||||
}
|
||||
},
|
||||
// 接收音频帧并处理
|
||||
frameRecorded({ isLastFrame, frameBuffer }) {
|
||||
if (this.ws) {
|
||||
try {
|
||||
this.ws.send({
|
||||
data: frameBuffer
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('发送音频帧失败:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
// 监听分贝值
|
||||
onCurrentDecibels(decibels) {
|
||||
// this.currentDecibels = decibels.toFixed(2);
|
||||
// console.log("当前分贝:", this.currentDecibels);
|
||||
},
|
||||
// 录音停止回调
|
||||
stopIt(base64) {
|
||||
this.stopRecordAndClean();
|
||||
console.log('录音停止,最终音频Base64:', base64?.substring(0, 50) + '...');
|
||||
},
|
||||
// 关闭通话
|
||||
closeTalking() {
|
||||
// this.overTalking();
|
||||
common.navigateBack();
|
||||
},
|
||||
// 结束通话生成报告
|
||||
overTalking() {
|
||||
partnerChatReportTrigger({
|
||||
execId: this.execId,
|
||||
traId: this.traId,
|
||||
talkingType: this.isPreview ? 'preview' : 'practice'
|
||||
}).then((res) => {
|
||||
common.navigateTo(
|
||||
'/pages/sparring/result?isOver=1&traId=' +
|
||||
this.traId +
|
||||
'&execId=' +
|
||||
this.execId +
|
||||
'&type=' +
|
||||
this.type +
|
||||
'&isPreview=' +
|
||||
(this.isPreview ? '1' : '')
|
||||
);
|
||||
});
|
||||
},
|
||||
switchChange(e) {
|
||||
this.sceneDescShow = !this.sceneDescShow;
|
||||
this.isConnecting = !this.isConnecting;
|
||||
},
|
||||
// 停止录音并清理资源
|
||||
stopRecordAndClean() {
|
||||
if (this.isRecording) {
|
||||
// 停止录音组件
|
||||
this.$refs.recordFrame.stop();
|
||||
this.isRecording = false;
|
||||
this.status = '已停止录音';
|
||||
}
|
||||
|
||||
// 关闭 WebSocket
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
// 清理音频上下文
|
||||
if (this.audioContext) {
|
||||
this.audioContext.close();
|
||||
this.audioContext = null;
|
||||
this.scriptProcessor = null;
|
||||
this.frameBufferList = [];
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
this.currentDecibels = 0;
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(e) {
|
||||
// 页面加载时初始化数据
|
||||
this.traId = e.traId ?? '';
|
||||
this.chaId = e.chaId ?? '';
|
||||
this.type = e.type ?? '';
|
||||
this.isPreview = e.isPreview === '1';
|
||||
this.userInfo = getUserInfo();
|
||||
// this.callPhone();
|
||||
initConnection({ traId: this.traId, chaId: this.chaId, type: this.type }, this.onStartRecord);
|
||||
},
|
||||
onBackPress(res) {
|
||||
return res.from === 'backbutton';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script module="renderJS" lang="renderjs">
|
||||
import { StreamPlayer } from "./js/StreamPlayer";
|
||||
let player = null;
|
||||
|
||||
export default {
|
||||
mounted() {
|
||||
const _this = this;
|
||||
player = new StreamPlayer({
|
||||
inputSampleRate: 44100, // 后端PCM采样率(如8000/24000)
|
||||
numChannels: 1, // 声道数(单声道/双声道)
|
||||
bitDepth: 16, // 位深(16/32bit)
|
||||
littleEndian: true, // 端序(和后端一致)
|
||||
pcmType: 'int', // 类型(int/float)
|
||||
callback: _this.callback
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
appendPCMChunk(numArray) {
|
||||
// console.log('接收到', numArray.length);
|
||||
if (!numArray || numArray.length === 0) return;
|
||||
// 数字数组转回ArrayBuffer
|
||||
const uint8 = new Uint8Array(numArray);
|
||||
const arrayBuffer = uint8.buffer;
|
||||
|
||||
player.appendChunk(arrayBuffer);
|
||||
},
|
||||
// 停止播放(销毁播放器)
|
||||
stop() {
|
||||
if (player) {
|
||||
player.destroy();
|
||||
player = null;
|
||||
// 重新初始化(方便后续再次播放)
|
||||
const _this = this;
|
||||
player = new StreamPlayer({
|
||||
inputSampleRate: 16000,
|
||||
numChannels: 1,
|
||||
bitDepth: 16,
|
||||
littleEndian: true,
|
||||
pcmType: 'int',
|
||||
callback: _this.callback
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 播放结束回调(通知主线程)
|
||||
callback(e) {
|
||||
if (e === 'ended') {
|
||||
this.$ownerInstance.callMethod('changeStreamPlaying', { type: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.switch-div {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-right: 20rpx;
|
||||
margin-top: 10rpx;
|
||||
width: 100%;
|
||||
.switch-text {
|
||||
font-weight: 400;
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
.switch {
|
||||
transform: scale(0.7) rotate(180deg);
|
||||
:deep(.uni-switch-input) {
|
||||
&.uni-switch-input-checked {
|
||||
border-color: #9eacbf !important;
|
||||
background-color: #9eacbf !important;
|
||||
}
|
||||
|
||||
&:after {
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
&:before {
|
||||
background-color: #62a1ff !important;
|
||||
border-color: #62a1ff !important;
|
||||
}
|
||||
&:not(.uni-switch-input-checked) {
|
||||
background-color: #62a1ff !important;
|
||||
border-color: #62a1ff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fl1-div-1 {
|
||||
min-height: 120rpx;
|
||||
max-height: 240rpx;
|
||||
}
|
||||
.show-box {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: linear-gradient(120deg, #333333cc 0%, #666666cc 50%, #333333cc 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.show-box-avatar {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 900;
|
||||
}
|
||||
|
||||
.role-info {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
padding-top: 210rpx;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
line-height: 58rpx;
|
||||
font-size: 38rpx;
|
||||
|
||||
.role-avatar {
|
||||
width: 185rpx;
|
||||
height: 185rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.role-name {
|
||||
margin-bottom: 26rpx;
|
||||
}
|
||||
|
||||
.title-tip {
|
||||
padding-right: 20rpx;
|
||||
position: relative;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
animation: loading 2.5s infinite;
|
||||
}
|
||||
@keyframes loading {
|
||||
0% {
|
||||
content: '';
|
||||
}
|
||||
25% {
|
||||
content: '.';
|
||||
}
|
||||
50% {
|
||||
content: '..';
|
||||
}
|
||||
75% {
|
||||
content: '...';
|
||||
}
|
||||
100% {
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
.title-tip {
|
||||
&:not(.connecting) {
|
||||
// display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body-scene {
|
||||
overflow: auto;
|
||||
width: calc(100% - 92rpx);
|
||||
// margin: 46rpx;
|
||||
padding: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
border: 4rpx dashed rgba(255, 255, 255, 0.3);
|
||||
border-radius: 16rpx;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
&.show {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 118rpx;
|
||||
height: 118rpx;
|
||||
background-image: url(@/static/images/sparring/close-talk.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
border-radius: 50%;
|
||||
margin-top: 46rpx;
|
||||
margin-bottom: 112rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,158 +0,0 @@
|
||||
<template>
|
||||
<video :is-live="true" :src="videoSrc" class="video-box" :autoplay="true" @error="error"></video>
|
||||
<view class="hidden">
|
||||
<live-pusher id="pusher-box" :enable-camera="false"></live-pusher>
|
||||
<!-- <button class="btn" @click="prepare">预热</button>
|
||||
<button class="btn" @click="start">开始推流1</button>
|
||||
<button class="btn" @click="pause">暂停推流</button>
|
||||
<button class="btn" @click="resume">恢复</button>
|
||||
<button class="btn" @click="stop">停止推流</button> -->
|
||||
<!-- <button class="btn" @click="snapshot">快照</button>
|
||||
<button class="btn" @click="startPreview">开启摄像头预览1</button>
|
||||
<button class="btn" @click="stopPreview">关闭摄像头预览</button>
|
||||
<button class="btn" @click="switchCamera">切换摄像头</button> -->
|
||||
<!-- <cover-view class="full-view"></cover-view> -->
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import {
|
||||
prepareApi
|
||||
} from "@/api/talk.js";
|
||||
import { onReady } from '@dcloudio/uni-app'
|
||||
var pusher = null;
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
baseName: 'wangwu',
|
||||
context: null,
|
||||
isGetting: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
videoSrc() {
|
||||
return 'rtmp://25.18.122.148:9605/voice_out/' + this.baseName;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// this.prepare()
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.isGetting = false
|
||||
this.stop()
|
||||
this.close()
|
||||
},
|
||||
methods: {
|
||||
statechange(e) {
|
||||
console.log('statechange:', e);
|
||||
let {
|
||||
detail
|
||||
} = e
|
||||
console.log(detail);
|
||||
switch (detail.code) {
|
||||
case '3004':
|
||||
console.log(3004);
|
||||
this.close()
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
netstatus(e) {
|
||||
console.log('netstatus');
|
||||
},
|
||||
error(e) {
|
||||
console.log('error:', e);
|
||||
},
|
||||
start() {
|
||||
console.log('start');
|
||||
pusher.start(a => {
|
||||
console.log('livePusher.start:');
|
||||
}, b => {
|
||||
console.log('livePusher.start.err:');
|
||||
});
|
||||
},
|
||||
close() {
|
||||
console.log('close');
|
||||
pusher.close();
|
||||
},
|
||||
snapshot() {},
|
||||
resume() {
|
||||
pusher.resume();
|
||||
},
|
||||
pause() {
|
||||
pusher.pause()
|
||||
},
|
||||
stop() {
|
||||
pusher.stop()
|
||||
},
|
||||
switchCamera() {},
|
||||
startPreview() {},
|
||||
stopPreview() {},
|
||||
prepare({traId, chaId, traTyp}) {
|
||||
if(!this.isGetting){
|
||||
this.isGetting = true
|
||||
return
|
||||
}
|
||||
prepareApi({
|
||||
traId: traId,
|
||||
chaId: chaId,
|
||||
traTyp: traTyp,
|
||||
}).then(res => {
|
||||
console.log(res)
|
||||
if(res.body.prepare){
|
||||
this.baseName = res.body.sid
|
||||
this.initPusher()
|
||||
}else{
|
||||
setTimeout(()=> {
|
||||
this.prepare({traId, chaId, traTyp})
|
||||
},2000)
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
},
|
||||
initPusher() {
|
||||
console.log('rtmp://25.18.122.148:9605/voice_in/' + this.baseName)
|
||||
pusher = new plus.video.LivePusher('pusher-box', {
|
||||
url: 'rtmp://25.18.122.148:9605/voice_in/' + this.baseName,
|
||||
"enable-camera": true,
|
||||
"min-bitrate": 16,
|
||||
"max-bitrate": 16,
|
||||
top: '100px',
|
||||
left: '0px',
|
||||
width: '1px',
|
||||
height: '1px',
|
||||
position: 'static'
|
||||
})
|
||||
pusher.addEventListener('statechange', this.statechange)
|
||||
pusher.addEventListener('netstatus', this.netstatus)
|
||||
pusher.addEventListener('error', this.error)
|
||||
console.log(pusher)
|
||||
this.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.hidden{
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
}
|
||||
.full-view{
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
position: sticky;
|
||||
flex: 1;
|
||||
/* background-color: #fff; */
|
||||
}
|
||||
.pusher-box{
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.video-box{
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user