feat: 直播中调用查询直播状态接口
This commit is contained in:
parent
3ba6221bd2
commit
efe57930aa
@ -1,5 +1,5 @@
|
|||||||
VITE_WS_URL = ws://43.136.52.196:27001/device
|
# VITE_WS_URL = ws://43.136.52.196:27001/device
|
||||||
# VITE_WS_URL = ws://ai.xuexiaole.com/ws/device
|
VITE_WS_URL = ws://ai.lingxixue.com/ws/device
|
||||||
VITE_OSS_URL = https://lxx-web-1313840333.cos.ap-chengdu.myqcloud.com
|
VITE_OSS_URL = https://lxx-web-1313840333.cos.ap-chengdu.myqcloud.com
|
||||||
VITE_CDN_URL = https://dmgcdn-1313840333.cos.ap-guangzhou.myqcloud.com
|
VITE_CDN_URL = https://dmgcdn-1313840333.cos.ap-guangzhou.myqcloud.com
|
||||||
VITE_SERVER_URL = http://127.0.0.1:9053/api/main
|
VITE_SERVER_URL = http://127.0.0.1:9053/api/main
|
||||||
|
|||||||
20
src/api/subject/teacherScreenShare.ts
Normal file
20
src/api/subject/teacherScreenShare.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { get } from '../request'
|
||||||
|
|
||||||
|
export type TeacherScreenShareState = {
|
||||||
|
classId: number
|
||||||
|
ip: string
|
||||||
|
lastHeartbeatAt: number
|
||||||
|
port: number
|
||||||
|
sharing: boolean
|
||||||
|
startedAt: number
|
||||||
|
ttlSeconds: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const teacherScreenShareApi = {
|
||||||
|
/** 查询当前班级老师投屏状态(直播中轮询) */
|
||||||
|
state: async (classId: string) => {
|
||||||
|
return await get<TeacherScreenShareState>('/school/teacherScreenShare/state', { classId })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default teacherScreenShareApi
|
||||||
@ -8,10 +8,12 @@
|
|||||||
* 4. 每 30 秒发送一次心跳保活
|
* 4. 每 30 秒发送一次心跳保活
|
||||||
* 5. 收到的消息统一处理,支持注册自定义消息回调
|
* 5. 收到的消息统一处理,支持注册自定义消息回调
|
||||||
* 6. 断线自动重连
|
* 6. 断线自动重连
|
||||||
|
* 7. 老师投屏进行中每 30s 查询 /school/teacherScreenShare/state,sharing 为 false 时结束投屏并停止轮询
|
||||||
*/
|
*/
|
||||||
import { ref, shallowRef } from 'vue'
|
import { ref, shallowRef } from 'vue'
|
||||||
import questionExplainApi from '@/api/subject/questionExplain'
|
import questionExplainApi from '@/api/subject/questionExplain'
|
||||||
import { getCache } from '@/utils'
|
import teacherScreenShareApi from '@/api/subject/teacherScreenShare'
|
||||||
|
import { getCache, $XXL } from '@/utils'
|
||||||
import { userStore } from '@/stores'
|
import { userStore } from '@/stores'
|
||||||
import { handleClassMonitorMessage, stopClassMonitor } from './useClassMonitor'
|
import { handleClassMonitorMessage, stopClassMonitor } from './useClassMonitor'
|
||||||
|
|
||||||
@ -20,6 +22,9 @@ import { handleClassMonitorMessage, stopClassMonitor } from './useClassMonitor'
|
|||||||
/** 心跳发送间隔(毫秒) */
|
/** 心跳发送间隔(毫秒) */
|
||||||
const HEARTBEAT_INTERVAL = 30_000
|
const HEARTBEAT_INTERVAL = 30_000
|
||||||
|
|
||||||
|
/** 接收老师投屏时,查询服务端投屏状态的间隔(毫秒) */
|
||||||
|
const TEACHER_SCREEN_SHARE_STATE_POLL_INTERVAL = 30_000
|
||||||
|
|
||||||
/** 断线重连延迟(毫秒) */
|
/** 断线重连延迟(毫秒) */
|
||||||
const RECONNECT_DELAY = 5_000
|
const RECONNECT_DELAY = 5_000
|
||||||
|
|
||||||
@ -58,6 +63,11 @@ let reconnectAttempts = 0
|
|||||||
let isManualClose = false
|
let isManualClose = false
|
||||||
let visibilityHandler: (() => void) | null = null
|
let visibilityHandler: (() => void) | null = null
|
||||||
|
|
||||||
|
/** 投屏状态轮询定时器(仅在收到 teacherScreenShareStart 后存在) */
|
||||||
|
let screenSharePollTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
/** 当前轮询对应的班级 id,用于丢弃过期异步结果 */
|
||||||
|
let screenSharePollClassId: string | null = null
|
||||||
|
|
||||||
/** 外部注册的消息处理回调集合 */
|
/** 外部注册的消息处理回调集合 */
|
||||||
const messageHandlers: Set<MessageHandler> = new Set()
|
const messageHandlers: Set<MessageHandler> = new Set()
|
||||||
|
|
||||||
@ -70,6 +80,100 @@ const lastMessage = shallowRef<TeacherWsMessage | null>(null)
|
|||||||
/** 班级信息(响应式) */
|
/** 班级信息(响应式) */
|
||||||
const classInfo = ref<any>(null)
|
const classInfo = ref<any>(null)
|
||||||
|
|
||||||
|
const TEACHER_SCREEN_SHARE_TYPES = new Set(['teacherScreenShareStart', 'teacherScreenShareStop'])
|
||||||
|
|
||||||
|
/** 兼容顶层或嵌套在 `data` 下的投屏消息 */
|
||||||
|
function resolveTeacherScreenSharePayload(data: TeacherWsMessage): TeacherWsMessage | null {
|
||||||
|
if (TEACHER_SCREEN_SHARE_TYPES.has(data.type)) return data
|
||||||
|
const inner = (data as Record<string, unknown>).data
|
||||||
|
if (
|
||||||
|
inner &&
|
||||||
|
typeof inner === 'object' &&
|
||||||
|
TEACHER_SCREEN_SHARE_TYPES.has((inner as TeacherWsMessage).type)
|
||||||
|
) {
|
||||||
|
return inner as TeacherWsMessage
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMsgObject(msg: unknown): Record<string, unknown> | null {
|
||||||
|
if (msg && typeof msg === 'object') return msg as Record<string, unknown>
|
||||||
|
if (typeof msg === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(msg) as Record<string, unknown>
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTeacherScreenSharePolling() {
|
||||||
|
if (screenSharePollTimer) {
|
||||||
|
clearInterval(screenSharePollTimer)
|
||||||
|
screenSharePollTimer = null
|
||||||
|
}
|
||||||
|
screenSharePollClassId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchTeacherScreenShareStateOnce(classId: string) {
|
||||||
|
try {
|
||||||
|
const res = await teacherScreenShareApi.state(classId)
|
||||||
|
if (screenSharePollClassId !== classId) return
|
||||||
|
if (res?.sharing === false) {
|
||||||
|
$XXL.stopTeacherShareScreen()
|
||||||
|
stopTeacherScreenSharePolling()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[TeacherExplain] 投屏状态查询失败:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仅在直播中轮询;直播结束或未直播不启动 */
|
||||||
|
function startTeacherScreenSharePolling(classId: string) {
|
||||||
|
const cid = String(classId).trim()
|
||||||
|
if (!cid) return
|
||||||
|
stopTeacherScreenSharePolling()
|
||||||
|
screenSharePollClassId = cid
|
||||||
|
screenSharePollTimer = setInterval(() => {
|
||||||
|
fetchTeacherScreenShareStateOnce(cid).catch(() => {})
|
||||||
|
}, TEACHER_SCREEN_SHARE_STATE_POLL_INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 老师投屏开始/结束:调原生桥接 */
|
||||||
|
function handleTeacherScreenShareMessage(data: TeacherWsMessage) {
|
||||||
|
const root = resolveTeacherScreenSharePayload(data)
|
||||||
|
if (!root) return
|
||||||
|
|
||||||
|
if (root.type === 'teacherScreenShareStart') {
|
||||||
|
const msg = parseMsgObject(root.msg)
|
||||||
|
if (!msg) return
|
||||||
|
const host = msg.ip !== undefined && msg.ip !== null ? String(msg.ip) : ''
|
||||||
|
const port = Number(msg.port)
|
||||||
|
if (!host || !Number.isFinite(port)) return
|
||||||
|
const u = userStore().userInfo
|
||||||
|
$XXL.startTeacherShareScreen({
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
name: String(u?.name || u?.nickName || ''),
|
||||||
|
id: String(u?.userId ?? ''),
|
||||||
|
})
|
||||||
|
const pollClassId =
|
||||||
|
msg.classId !== undefined && msg.classId !== null && String(msg.classId).trim() !== ''
|
||||||
|
? String(msg.classId)
|
||||||
|
: classInfo.value?.classId !== undefined && classInfo.value?.classId !== null
|
||||||
|
? String(classInfo.value.classId)
|
||||||
|
: ''
|
||||||
|
if (pollClassId) startTeacherScreenSharePolling(pollClassId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.type === 'teacherScreenShareStop') {
|
||||||
|
stopTeacherScreenSharePolling()
|
||||||
|
$XXL.stopTeacherShareScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ======================== 内部方法 ========================
|
// ======================== 内部方法 ========================
|
||||||
|
|
||||||
/** 获取带 token 的 WebSocket 连接地址 */
|
/** 获取带 token 的 WebSocket 连接地址 */
|
||||||
@ -196,6 +300,7 @@ function handleMessage(data: TeacherWsMessage) {
|
|||||||
if (data.type === 'pong') return
|
if (data.type === 'pong') return
|
||||||
|
|
||||||
handleClassMonitorMessage(data)
|
handleClassMonitorMessage(data)
|
||||||
|
handleTeacherScreenShareMessage(data)
|
||||||
|
|
||||||
// 调用所有已注册的外部消息回调
|
// 调用所有已注册的外部消息回调
|
||||||
messageHandlers.forEach(handler => {
|
messageHandlers.forEach(handler => {
|
||||||
@ -288,6 +393,7 @@ async function init() {
|
|||||||
function close() {
|
function close() {
|
||||||
isManualClose = true
|
isManualClose = true
|
||||||
stopHeartbeat()
|
stopHeartbeat()
|
||||||
|
stopTeacherScreenSharePolling()
|
||||||
stopClassMonitor()
|
stopClassMonitor()
|
||||||
clearReconnectTimer()
|
clearReconnectTimer()
|
||||||
stopVisibilityListener()
|
stopVisibilityListener()
|
||||||
|
|||||||
@ -104,6 +104,11 @@ export const $XXL = {
|
|||||||
writeError: (error: string) => packSync('writeError', error),
|
writeError: (error: string) => packSync('writeError', error),
|
||||||
uninstallApp: (pkgName: string) => packSync('uninstallApp', pkgName),
|
uninstallApp: (pkgName: string) => packSync('uninstallApp', pkgName),
|
||||||
openExternalURL: (url: string) => packSync('openExternalURL', url),
|
openExternalURL: (url: string) => packSync('openExternalURL', url),
|
||||||
|
/** 老师投屏开始:参数为 JSON 字符串,与原生 `startTeacherShareScreen` 约定一致 */
|
||||||
|
startTeacherShareScreen: (config: { host: string; port: number; name: string; id: string }) =>
|
||||||
|
packSync('startTeacherShareScreen', JSON.stringify(config)),
|
||||||
|
/** 老师投屏结束 */
|
||||||
|
stopTeacherShareScreen: () => packSync('stopTeacherShareScreen'),
|
||||||
// openApp: (config: anyObj) => pack('openApp', config),
|
// openApp: (config: anyObj) => pack('openApp', config),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -148,16 +148,16 @@ export default defineConfig({
|
|||||||
open: true,
|
open: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api/campus': {
|
'/api/campus': {
|
||||||
target: 'http://43.136.52.196:9053',
|
// target: 'http://43.136.52.196:9053',
|
||||||
// target: 'http://test.web.xuexiaole.com',
|
// target: 'http://test.web.xuexiaole.com',
|
||||||
// target: 'https://ai.xuexiaole.com',
|
target: 'https://ai.lingxixue.com',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: path => path.replace('/api/campus', '/'),
|
rewrite: path => path.replace('/api/campus', '/'),
|
||||||
},
|
},
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://43.136.52.196:9053',
|
// target: 'http://43.136.52.196:9053',
|
||||||
// target: 'http://test.web.xuexiaole.com',
|
// target: 'http://test.web.xuexiaole.com',
|
||||||
// target: 'https://ai.xuexiaole.com',
|
target: 'https://ai.lingxixue.com',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user