Merge branch 'develop' of http://25.13.9.101:9000/K17_AITS/tra-app into develop

This commit is contained in:
田岩
2025-11-26 14:08:34 +08:00
7 changed files with 495 additions and 308 deletions
+10
View File
@@ -0,0 +1,10 @@
import request from '@/api/request'
export const prepareApi = (name) => {
return request({
url: '/trapractice/audio/session/prepare?name=' + name,
method: 'post',
toastErrors: true,
baseUrl: 'http://25.64.32.157:9603'
});
};
+1 -1
View File
@@ -14,7 +14,7 @@
<view class="body-top-title">
{{ detailInfo.traName }}
</view>
<view class="body-top-item"> 已练次数{{ detailInfo.practiceTimes }} </view>
<view class="body-top-item"> 练次数{{ detailInfo.practiceTimes }} </view>
<view class="body-top-item"> 类型{{ detailInfo.traInterTypDesc }} </view>
<view class="body-top-item"> 截止时间{{detailInfo.limitTyp === '01' ? '不限制' : detailInfo.endTm}}</view>
<scroll-view class="body-top-tags" :scroll-x="true">
+38 -27
View File
@@ -17,7 +17,7 @@
<uv-tabs :list="tabList" @click="changeTab" :current="currentTab" :scrollable="true" class="font_pf my_tabs"
:inactive-style="{ color: '#333333', fontSize: '30rpx', fontWeight: '500' }"
:activeStyle="{ color: '#333333', fontSize: '30rpx', fontWeight: '600' }"></uv-tabs>
<uv-tabs :list="tabSecList" @click="changeTabSec" :current="currentTabSec" :scrollable="true"
<uv-tabs :list="tabSecList" @click="changeTabSec" :current="currentTabSec" :scrollable="true" v-if="!resetingSec"
class="font_pf my_tabs2" :inactive-style="{ color: '#333333', fontSize: '24rpx', fontWeight: '500' }"
:activeStyle="{ color: '#0066FF', fontSize: '24rpx', fontWeight: '500' }" :lineColor="`#06f`"></uv-tabs>
<!-- :lineColor="`url(${LineBg}) 2% 2%`" -->
@@ -295,32 +295,37 @@
const refresherEnabled = ref(true)
const changeTab = item => {
if (!item) return
triggered.value = true
refresherEnabled.value = false
sparringListBase.value = []
activeTab.value = item.name
activeTabId.value = item.value || ''
activeLeftIndex.value = -1
activeTag.value = []
pageObj.value.page = 1
pageObj.value.total = 0
// activeTag.value = ''
activePrdlineType.value = ''
loading.value = true
console.log(111)
queryCrsInfoByCatalogApi({
...pageObj.value,
catalogId: activeCourseType.value
})
.then(res => {
sparringListBase.value = res.body || []
pageObj.value.total = res.total
})
.finally(() => {
loading.value = false
triggered.value = false
refresherEnabled.value = true
})
resetingSec.value = true
setTimeout(() => {
resetingSec.value = false
triggered.value = true
refresherEnabled.value = false
sparringListBase.value = []
currentTabSec.value = 0
activeTab.value = item.name
activeTabId.value = item.value || ''
activeLeftIndex.value = -1
activeTag.value = []
pageObj.value.page = 1
pageObj.value.total = 0
// activeTag.value = ''
activePrdlineType.value = ''
loading.value = true
console.log(111)
queryCrsInfoByCatalogApi({
...pageObj.value,
catalogId: activeCourseType.value
})
.then(res => {
sparringListBase.value = res.body || []
pageObj.value.total = res.total
})
.finally(() => {
loading.value = false
triggered.value = false
refresherEnabled.value = true
})
})
}
const changeTabSec = (item) => {
sparringListBase.value = []
@@ -365,7 +370,9 @@
}
}
const reSeting = ref(false)
const resetingSec = ref(false)
onMounted(() => {
console.log('onMounted')
reSeting.value = true
setTimeout(() => {
reSeting.value = false
@@ -395,6 +402,7 @@
uni.$off('refresh_sparring_list')
})
onActivated(() => {
console.log('onActivated')
reSeting.value = true
setTimeout(() => {
reSeting.value = false
@@ -413,6 +421,9 @@
})
})
onShow(() => {
console.log('onShow')
activeTag.value = []
getMoreList(1)
socketStore.closeSocket()
uni.$on('refresh_sparring_list', () => {
changeTab({
+168 -168
View File
@@ -1,182 +1,182 @@
<template>
<view class="container">
<button @click="startPush" v-if="!isPushing">开始语音推流</button>
<button @click="stopPush" v-else>停止语音推流</button>
</view>
<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;
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();
try {
await this.requestMicPermission()
// 优先用原生 API 获取 Webview(兜底)
this.currentWebview = plus.webview.currentWebview() || this.$mp.page.$getAppWebview();
if (!this.currentWebview) {
throw new Error('无法获取页面 Webview 实例');
}
// 优先用原生 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);
// 后续创建 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;
}
});
// 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);
}
},
// 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' });
}
}
};
// 停止推流
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;
}
.container {
padding: 20px;
}
button {
height: 80rpx;
line-height: 80rpx;
background: #007aff;
color: #fff;
border-radius: 10rpx;
margin-bottom: 20rpx;
}
</style>
-112
View File
@@ -1,112 +0,0 @@
<!-- <template>
<view>通话</view>
<live-pusher
url="rtmp://25.18.122.148:9605/voice_in/aa"
:enable-camera="true"
@statechange="statechange"
@netstatus="netstatus"
@error="errorFnc"
></live-pusher>
</template> -->
<template>
<div>
<video :is-live="true" :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>
<text class="btn" @click="start">开始推流</text>
<text class="btn" @click="pause">暂停推流</text>
<text class="btn" @click="resume">resume</text>
<text class="btn" @click="stop">停止推流</text>
<text class="btn" @click="snapshot">快照</text>
<text class="btn" @click="startPreview">开启摄像头预览1</text>
<text class="btn" @click="stopPreview">关闭摄像头预览</text>
<text class="btn" @click="switchCamera">切换摄像头</text>
</div>
</template>
<script>
export default {
data() {
return {
videoSrc: 'rtmp://25.18.122.148:9605/voice_out/aa'
}
},
onReady() {
// 注意:需要在onReady中 或 onLoad 延时
this.context = uni.createLivePusherContext('livePusher', this)
},
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 () {
this.context.start({
success: a => {
console.log('livePusher.start:' + JSON.stringify(a))
}
})
},
close: function () {
this.context.close({
success: a => {
console.log('livePusher.close:' + JSON.stringify(a))
}
})
},
snapshot: function () {
this.context.snapshot({
success: e => {
console.log(JSON.stringify(e))
}
})
},
resume: function () {
this.context.resume({
success: a => {
console.log('livePusher.resume:' + JSON.stringify(a))
}
})
},
pause: function () {
this.context.pause({
success: a => {
console.log('livePusher.pause:' + JSON.stringify(a))
}
})
},
stop: function () {
this.context.stop({
success: a => {
console.log(JSON.stringify(a))
}
})
},
switchCamera: function () {
this.context.switchCamera({
success: a => {
console.log('livePusher.switchCamera:' + JSON.stringify(a))
}
})
},
startPreview: function () {
this.context.startPreview({
success: a => {
console.log('livePusher.startPreview:' + JSON.stringify(a))
}
})
},
stopPreview: function () {
this.context.stopPreview({
success: a => {
console.log('livePusher.stopPreview:' + JSON.stringify(a))
}
})
}
}
}
</script>
<style></style>
+147
View File
@@ -0,0 +1,147 @@
<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>
+131
View File
@@ -0,0 +1,131 @@
<template>
<view>
<video :is-live="true" :src="videoSrc" :autoplay="true" @error="error"></video>
<view id="pusher-box"></view>
<button class="btn" @click="start">开始推流</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>
</view>
</template>
<script>
import { prepareApi } from "@/api/talk.js"
var pusher;
export default {
data() {
return {
baseName: 'zhangsan',
context: null
}
},
computed:{
videoSrc(){
return 'rtmp://25.18.122.148:9605/voice_out/' + this.baseName
},
},
onReady() {
},
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('start')
console.log('start',pusher.start)
pusher.start(a => {
console.log('livePusher.start:')
},b=>{
console.log('livePusher.start.err:' )
})
},
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 () {
prepareApi(this.baseName).then(res=>{
console.log(res)
}).catch(err=>{
console.log(err)
if(err === 'prepare'){
this.initPusher()
}
})
},
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 scoped lang="scss">
#pusher-box{
width: 100vw;
height: 400rpx;
}
</style>