fix: 去掉直播30秒轮询,课堂监控调用原生监控方法
This commit is contained in:
parent
9655f4742a
commit
49023a433d
@ -11,7 +11,7 @@ export type TeacherScreenShareState = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const teacherScreenShareApi = {
|
const teacherScreenShareApi = {
|
||||||
/** 查询当前班级老师投屏状态(直播中轮询) */
|
/** 查询当前班级老师投屏状态 */
|
||||||
state: async (classId: string) => {
|
state: async (classId: string) => {
|
||||||
return await get<TeacherScreenShareState>('/school/teacherScreenShare/state', { classId })
|
return await get<TeacherScreenShareState>('/school/teacherScreenShare/state', { classId })
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,28 +1,16 @@
|
|||||||
import html2canvas from 'html2canvas'
|
import { $XXL } from '@/utils'
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import globalApi from '@/api/global'
|
|
||||||
import { userStore } from '@/stores'
|
|
||||||
import { $XXL, getNativeApi } from '@/utils'
|
|
||||||
import { useTips } from './useTips'
|
import { useTips } from './useTips'
|
||||||
import { uploadFileToCos, uploadNativeFilePathToCos } from '@/utils/cosUpload'
|
|
||||||
|
|
||||||
let monitorTimer: ReturnType<typeof setInterval> | null = null
|
|
||||||
let monitorSessionId = ''
|
let monitorSessionId = ''
|
||||||
let isUploadingMonitorSnapshot = false
|
|
||||||
let monitorState: 0 | 1 = 0
|
let monitorState: 0 | 1 = 0
|
||||||
let closeMonitorTip: (() => void) | null = null
|
let closeMonitorTip: (() => void) | null = null
|
||||||
|
|
||||||
const MONITOR_SNAPSHOT_INTERVAL = 10000
|
|
||||||
const MONITOR_MSG_TYPE = 'monitorStart'
|
const MONITOR_MSG_TYPE = 'monitorStart'
|
||||||
const MONITOR_STOP_MSG_TYPE = 'monitorStop'
|
const MONITOR_STOP_MSG_TYPE = 'monitorStop'
|
||||||
const MONITOR_LOG_PREFIX = '[class-monitor]'
|
const MONITOR_LOG_PREFIX = '[class-monitor]'
|
||||||
const MONITOR_SNAPSHOT_TARGET_SIZE = 100 * 1024
|
|
||||||
const MONITOR_SNAPSHOT_MIN_WIDTH = 480
|
|
||||||
const MONITOR_SNAPSHOT_MIN_HEIGHT = 270
|
|
||||||
|
|
||||||
const monitorLog = (...args: unknown[]) => console.log(MONITOR_LOG_PREFIX, ...args)
|
const monitorLog = (...args: unknown[]) => console.log(MONITOR_LOG_PREFIX, ...args)
|
||||||
const monitorWarn = (...args: unknown[]) => console.warn(MONITOR_LOG_PREFIX, ...args)
|
const monitorWarn = (...args: unknown[]) => console.warn(MONITOR_LOG_PREFIX, ...args)
|
||||||
const monitorError = (...args: unknown[]) => console.error(MONITOR_LOG_PREFIX, ...args)
|
|
||||||
|
|
||||||
const showMonitorTip = (tips: string) => {
|
const showMonitorTip = (tips: string) => {
|
||||||
closeMonitorTip?.()
|
closeMonitorTip?.()
|
||||||
@ -32,336 +20,14 @@ const showMonitorTip = (tips: string) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getServerOrigin = () => {
|
const startStudentShareScreen = () => {
|
||||||
try {
|
monitorLog('call native startStudentShareScreen')
|
||||||
return new URL(import.meta.env.VITE_SERVER_URL).origin
|
$XXL.startStudentShareScreen()
|
||||||
} catch {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLocalFilePath = (path: string) => {
|
const stopStudentShareScreen = () => {
|
||||||
return /^(file:\/\/|\/storage\/|\/sdcard\/|content:\/\/)/i.test(path)
|
monitorLog('call native stopStudentShareScreen')
|
||||||
}
|
$XXL.stopStudentShareScreen()
|
||||||
|
|
||||||
const normalizeUploadPath = (res: any) => {
|
|
||||||
const rawPath = String(
|
|
||||||
res?.data?.url ||
|
|
||||||
res?.url ||
|
|
||||||
res?.data?.filePath ||
|
|
||||||
res?.filePath ||
|
|
||||||
res?.data?.path ||
|
|
||||||
res?.path ||
|
|
||||||
'',
|
|
||||||
)
|
|
||||||
.replace(/[`]/g, '')
|
|
||||||
.replace(/http:\/\//gi, 'https://')
|
|
||||||
.trim()
|
|
||||||
|
|
||||||
if (!rawPath) return ''
|
|
||||||
if (isLocalFilePath(rawPath)) {
|
|
||||||
monitorWarn('upload result is local path, ignore it', rawPath)
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
if (/^https?:\/\//i.test(rawPath)) return rawPath
|
|
||||||
|
|
||||||
const origin = getServerOrigin()
|
|
||||||
if (origin && rawPath.startsWith('/')) return `${origin}${rawPath}`
|
|
||||||
return rawPath
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseNativeScreenshotPath = (raw: unknown): string => {
|
|
||||||
if (typeof raw === 'string' && raw.trim()) return raw.trim()
|
|
||||||
if (!raw || typeof raw !== 'object') return ''
|
|
||||||
const r = raw as Record<string, unknown>
|
|
||||||
const directPath = r.filePath ?? r.path ?? r.localPath ?? r.url
|
|
||||||
if (typeof directPath === 'string' && directPath.trim()) return directPath.trim()
|
|
||||||
const d = r.data
|
|
||||||
if (typeof d === 'string' && d.trim()) return d.trim()
|
|
||||||
if (d && typeof d === 'object') {
|
|
||||||
const o = d as Record<string, unknown>
|
|
||||||
const p = o.filePath ?? o.path ?? o.localPath
|
|
||||||
if (typeof p === 'string' && p.trim()) return p.trim()
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadNativeScreenshot = async () => {
|
|
||||||
try {
|
|
||||||
const raw = $XXL.getScreenshot() || {}
|
|
||||||
monitorLog('native getScreenshot result', raw)
|
|
||||||
const filePath = parseNativeScreenshotPath(raw)
|
|
||||||
if (!filePath) {
|
|
||||||
monitorWarn('native screenshot path empty')
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
monitorLog('native screenshot path', filePath)
|
|
||||||
|
|
||||||
const res = await uploadNativeFilePathToCos(filePath, config => $XXL.uploadFile(config), {
|
|
||||||
fileType: 1,
|
|
||||||
})
|
|
||||||
const url = normalizeUploadPath(res)
|
|
||||||
monitorLog('native upload result', { raw: res, url })
|
|
||||||
return url
|
|
||||||
} catch (err) {
|
|
||||||
monitorWarn('native screenshot upload failed', err)
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** App WebView 无原生截图时仅能用 DOM 截图;尽量贴合「当前视口」并降低高 DPR 白屏概率 */
|
|
||||||
const getMonitorCaptureElement = (): HTMLElement => {
|
|
||||||
const app = document.getElementById('app')
|
|
||||||
if (app && (app.offsetWidth > 0 || app.offsetHeight > 0)) return app
|
|
||||||
const scrollRoot = document.scrollingElement
|
|
||||||
if (scrollRoot instanceof HTMLElement) return scrollRoot
|
|
||||||
return document.documentElement
|
|
||||||
}
|
|
||||||
|
|
||||||
const prepareMonitorDomForCapture = async () => {
|
|
||||||
try {
|
|
||||||
await document.fonts?.ready
|
|
||||||
} catch {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
await new Promise<void>(resolve => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
requestAnimationFrame(() => resolve())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const canvasToBlob = (canvas: HTMLCanvasElement, type: string, quality?: number) =>
|
|
||||||
new Promise<Blob | null>(resolve => {
|
|
||||||
canvas.toBlob(
|
|
||||||
blob => {
|
|
||||||
resolve(blob)
|
|
||||||
},
|
|
||||||
type,
|
|
||||||
quality,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const resizeCanvas = (source: HTMLCanvasElement, ratio: number) => {
|
|
||||||
const canvas = document.createElement('canvas')
|
|
||||||
canvas.width = Math.max(1, Math.round(source.width * ratio))
|
|
||||||
canvas.height = Math.max(1, Math.round(source.height * ratio))
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
if (!ctx) return source
|
|
||||||
ctx.drawImage(source, 0, 0, canvas.width, canvas.height)
|
|
||||||
return canvas
|
|
||||||
}
|
|
||||||
|
|
||||||
const canvasToMonitorFile = async (canvas: HTMLCanvasElement) => {
|
|
||||||
const qualities = [0.72, 0.6, 0.5, 0.42, 0.35, 0.3, 0.25]
|
|
||||||
let current = canvas
|
|
||||||
let bestBlob: Blob | null = null
|
|
||||||
let bestInfo = {
|
|
||||||
width: canvas.width,
|
|
||||||
height: canvas.height,
|
|
||||||
quality: qualities[0],
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let resizeCount = 0; resizeCount < 8; resizeCount++) {
|
|
||||||
for (const quality of qualities) {
|
|
||||||
const blob = await canvasToBlob(current, 'image/jpeg', quality)
|
|
||||||
if (!blob) return null
|
|
||||||
if (!bestBlob || blob.size < bestBlob.size) {
|
|
||||||
bestBlob = blob
|
|
||||||
bestInfo = {
|
|
||||||
width: current.width,
|
|
||||||
height: current.height,
|
|
||||||
quality,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (blob.size <= MONITOR_SNAPSHOT_TARGET_SIZE) {
|
|
||||||
monitorLog('web screenshot compressed', {
|
|
||||||
width: current.width,
|
|
||||||
height: current.height,
|
|
||||||
quality,
|
|
||||||
size: blob.size,
|
|
||||||
})
|
|
||||||
return new File([blob], `monitor-${Date.now()}.jpg`, { type: 'image/jpeg' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
current.width <= MONITOR_SNAPSHOT_MIN_WIDTH ||
|
|
||||||
current.height <= MONITOR_SNAPSHOT_MIN_HEIGHT
|
|
||||||
) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
current = resizeCanvas(current, 0.85)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bestBlob) return null
|
|
||||||
monitorWarn('web screenshot compressed but still over target', {
|
|
||||||
...bestInfo,
|
|
||||||
size: bestBlob.size,
|
|
||||||
target: MONITOR_SNAPSHOT_TARGET_SIZE,
|
|
||||||
})
|
|
||||||
return new File([bestBlob], `monitor-${Date.now()}.jpg`, { type: 'image/jpeg' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const createWebScreenshotFile = async (): Promise<File | null> => {
|
|
||||||
await prepareMonitorDomForCapture()
|
|
||||||
|
|
||||||
const vw = window.innerWidth
|
|
||||||
const vh = window.innerHeight
|
|
||||||
const scale = Math.min(window.devicePixelRatio || 1, 1)
|
|
||||||
monitorLog('web screenshot start', {
|
|
||||||
href: window.location.href,
|
|
||||||
viewport: `${vw}x${vh}`,
|
|
||||||
dpr: window.devicePixelRatio,
|
|
||||||
scale,
|
|
||||||
scrollX: window.scrollX,
|
|
||||||
scrollY: window.scrollY,
|
|
||||||
})
|
|
||||||
const base = {
|
|
||||||
useCORS: true,
|
|
||||||
allowTaint: false,
|
|
||||||
backgroundColor: '#ffffff' as const,
|
|
||||||
scale,
|
|
||||||
imageTimeout: 15000,
|
|
||||||
logging: import.meta.env.DEV,
|
|
||||||
}
|
|
||||||
const win = {
|
|
||||||
scrollX: -window.scrollX,
|
|
||||||
scrollY: -window.scrollY,
|
|
||||||
windowWidth: Math.max(document.documentElement.clientWidth, vw),
|
|
||||||
windowHeight: Math.max(document.documentElement.clientHeight, vh),
|
|
||||||
}
|
|
||||||
|
|
||||||
const attempts: Array<() => Promise<HTMLCanvasElement>> = [
|
|
||||||
// 视口截图常用写法(对 WebView 比单纯裁 x/y 更稳)
|
|
||||||
() =>
|
|
||||||
html2canvas(document.body, {
|
|
||||||
...base,
|
|
||||||
foreignObjectRendering: false,
|
|
||||||
...win,
|
|
||||||
}),
|
|
||||||
() =>
|
|
||||||
html2canvas(document.body, {
|
|
||||||
...base,
|
|
||||||
foreignObjectRendering: true,
|
|
||||||
...win,
|
|
||||||
}),
|
|
||||||
() => {
|
|
||||||
const el = getMonitorCaptureElement()
|
|
||||||
const sx = window.scrollX
|
|
||||||
const sy = window.scrollY
|
|
||||||
return html2canvas(el, {
|
|
||||||
...base,
|
|
||||||
foreignObjectRendering: false,
|
|
||||||
x: sx,
|
|
||||||
y: sy,
|
|
||||||
width: vw,
|
|
||||||
height: vh,
|
|
||||||
windowWidth: vw,
|
|
||||||
windowHeight: vh,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const [index, run] of attempts.entries()) {
|
|
||||||
try {
|
|
||||||
monitorLog('web screenshot attempt start', index + 1)
|
|
||||||
const canvas = await run()
|
|
||||||
monitorLog('web screenshot canvas', {
|
|
||||||
attempt: index + 1,
|
|
||||||
width: canvas.width,
|
|
||||||
height: canvas.height,
|
|
||||||
})
|
|
||||||
if (!canvas.width || !canvas.height) {
|
|
||||||
monitorWarn('web screenshot canvas empty', index + 1)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const file = await canvasToMonitorFile(canvas)
|
|
||||||
if (file) {
|
|
||||||
monitorLog('web screenshot file ready', {
|
|
||||||
attempt: index + 1,
|
|
||||||
name: file.name,
|
|
||||||
size: file.size,
|
|
||||||
type: file.type,
|
|
||||||
})
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
monitorWarn('web screenshot toBlob returned empty', index + 1)
|
|
||||||
} catch (err) {
|
|
||||||
monitorWarn('web screenshot attempt failed', index + 1, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
monitorWarn('web screenshot all attempts failed')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadWebScreenshot = async () => {
|
|
||||||
monitorLog('web upload start')
|
|
||||||
const file = await createWebScreenshotFile()
|
|
||||||
if (!file) {
|
|
||||||
monitorWarn('web upload skipped because screenshot file is empty')
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await uploadFileToCos(file, { fileType: 1 })
|
|
||||||
const url = normalizeUploadPath(res)
|
|
||||||
monitorLog('web upload result', { raw: res, url })
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadMonitorScreenshot = async () => {
|
|
||||||
// $xxl(学习机)与 $xxl_universal(通用 App)都可能提供 getScreenshot,不能只用 sys_type 区分
|
|
||||||
const nativeApi = getNativeApi()
|
|
||||||
monitorLog('choose screenshot mode', {
|
|
||||||
hasNativeApi: !!nativeApi,
|
|
||||||
hasGetScreenshot: typeof nativeApi?.getScreenshot === 'function',
|
|
||||||
hasUploadFile: typeof nativeApi?.uploadFile === 'function',
|
|
||||||
})
|
|
||||||
if (nativeApi && typeof nativeApi.getScreenshot === 'function') {
|
|
||||||
const url = await uploadNativeScreenshot()
|
|
||||||
if (url) return url
|
|
||||||
monitorWarn('native screenshot empty, fallback to web screenshot')
|
|
||||||
}
|
|
||||||
return await uploadWebScreenshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
const reportMonitorSnapshot = async (sessionId: string) => {
|
|
||||||
const { userInfo } = storeToRefs(userStore())
|
|
||||||
if (isUploadingMonitorSnapshot || monitorSessionId !== sessionId) {
|
|
||||||
monitorLog('snapshot skipped', {
|
|
||||||
isUploadingMonitorSnapshot,
|
|
||||||
monitorSessionId,
|
|
||||||
sessionId,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isUploadingMonitorSnapshot = true
|
|
||||||
monitorLog('snapshot report start', { sessionId, userId: userInfo.value.userId })
|
|
||||||
try {
|
|
||||||
const url = await uploadMonitorScreenshot()
|
|
||||||
if (!url || monitorSessionId !== sessionId) {
|
|
||||||
monitorWarn('snapshot report skipped after screenshot', {
|
|
||||||
url,
|
|
||||||
monitorSessionId,
|
|
||||||
sessionId,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
monitorLog('snapshot final image url', url)
|
|
||||||
|
|
||||||
await globalApi.reportSnapshot({
|
|
||||||
sessionId,
|
|
||||||
url,
|
|
||||||
userId: userInfo.value.userId,
|
|
||||||
})
|
|
||||||
monitorLog('snapshot report success', { sessionId, url })
|
|
||||||
} catch (err) {
|
|
||||||
monitorError('snapshot report failed', err)
|
|
||||||
} finally {
|
|
||||||
isUploadingMonitorSnapshot = false
|
|
||||||
monitorLog('snapshot report end', { sessionId })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 与老师讲解 WS 等场景兼容:外层 type 可能是 push/classMessage,监控类型在 msg.type */
|
/** 与老师讲解 WS 等场景兼容:外层 type 可能是 push/classMessage,监控类型在 msg.type */
|
||||||
@ -374,11 +40,10 @@ export const resolveMonitorWsType = (data: anyObj): string | undefined => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const stopClassMonitor = (showTip = false) => {
|
export const stopClassMonitor = (showTip = false) => {
|
||||||
const isMonitoring = !!monitorTimer || !!monitorSessionId || monitorState === 1
|
const isMonitoring = !!monitorSessionId || monitorState === 1
|
||||||
monitorLog('stop monitor', { showTip, isMonitoring, monitorSessionId, monitorState })
|
monitorLog('stop monitor', { showTip, isMonitoring, monitorSessionId, monitorState })
|
||||||
if (monitorTimer) {
|
if (monitorState === 1) {
|
||||||
clearInterval(monitorTimer)
|
stopStudentShareScreen()
|
||||||
monitorTimer = null
|
|
||||||
}
|
}
|
||||||
if (showTip && isMonitoring) {
|
if (showTip && isMonitoring) {
|
||||||
showMonitorTip('已结束课堂监控')
|
showMonitorTip('已结束课堂监控')
|
||||||
@ -394,7 +59,7 @@ const startClassMonitor = (msg: anyObj) => {
|
|||||||
monitorWarn('start monitor ignored because sessionId is empty')
|
monitorWarn('start monitor ignored because sessionId is empty')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (monitorTimer && monitorSessionId === sessionId) {
|
if (monitorState === 1 && monitorSessionId === sessionId) {
|
||||||
monitorLog('start monitor ignored because session is already running', sessionId)
|
monitorLog('start monitor ignored because session is already running', sessionId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -403,8 +68,7 @@ const startClassMonitor = (msg: anyObj) => {
|
|||||||
monitorSessionId = sessionId
|
monitorSessionId = sessionId
|
||||||
monitorState = 1
|
monitorState = 1
|
||||||
showMonitorTip('已进入课堂监控状态')
|
showMonitorTip('已进入课堂监控状态')
|
||||||
reportMonitorSnapshot(sessionId)
|
startStudentShareScreen()
|
||||||
monitorTimer = setInterval(() => reportMonitorSnapshot(sessionId), MONITOR_SNAPSHOT_INTERVAL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMonitorSessionId = (data: anyObj) => {
|
const getMonitorSessionId = (data: anyObj) => {
|
||||||
@ -458,7 +122,10 @@ export const syncClassMonitorState = (state: unknown) => {
|
|||||||
const sessionId = getMonitorSessionId(stateInfo)
|
const sessionId = getMonitorSessionId(stateInfo)
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
monitorWarn('sync monitor state ignored because sessionId is empty', stateInfo)
|
monitorWarn('sync monitor state ignored because sessionId is empty', stateInfo)
|
||||||
|
if (monitorState !== 1) {
|
||||||
monitorState = 1
|
monitorState = 1
|
||||||
|
startStudentShareScreen()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
* 4. 每 30 秒发送一次心跳保活
|
* 4. 每 30 秒发送一次心跳保活
|
||||||
* 5. 收到的消息统一处理,支持注册自定义消息回调
|
* 5. 收到的消息统一处理,支持注册自定义消息回调
|
||||||
* 6. 断线自动重连
|
* 6. 断线自动重连
|
||||||
* 7. 老师投屏进行中每 30s 查询 /school/teacherScreenShare/state,sharing 为 false 时结束投屏并停止轮询
|
* 7. 老师投屏 start/stop 由 WebSocket 消息驱动
|
||||||
*/
|
*/
|
||||||
import { ref, shallowRef } from 'vue'
|
import { ref, shallowRef } from 'vue'
|
||||||
import questionExplainApi from '@/api/subject/questionExplain'
|
import questionExplainApi from '@/api/subject/questionExplain'
|
||||||
@ -22,9 +22,6 @@ 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
|
||||||
|
|
||||||
@ -63,11 +60,6 @@ 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()
|
||||||
|
|
||||||
@ -108,38 +100,6 @@ function parseMsgObject(msg: unknown): Record<string, unknown> | null {
|
|||||||
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 resolveTeacherScreenSharePollClassId(msg: Record<string, unknown>): string {
|
function resolveTeacherScreenSharePollClassId(msg: Record<string, unknown>): string {
|
||||||
if (msg.classId !== undefined && msg.classId !== null && String(msg.classId).trim() !== '') {
|
if (msg.classId !== undefined && msg.classId !== null && String(msg.classId).trim() !== '') {
|
||||||
return String(msg.classId)
|
return String(msg.classId)
|
||||||
@ -154,7 +114,7 @@ function resolveTeacherScreenSharePollClassId(msg: Record<string, unknown>): str
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 启动老师投屏直播(原生 + 状态轮询) */
|
/** 启动老师投屏直播(仅原生) */
|
||||||
function applyTeacherScreenShareStart(msg: Record<string, unknown>): boolean {
|
function applyTeacherScreenShareStart(msg: Record<string, unknown>): boolean {
|
||||||
const host = msg.ip !== undefined && msg.ip !== null ? String(msg.ip) : ''
|
const host = msg.ip !== undefined && msg.ip !== null ? String(msg.ip) : ''
|
||||||
const port = Number(msg.port)
|
const port = Number(msg.port)
|
||||||
@ -167,13 +127,10 @@ function applyTeacherScreenShareStart(msg: Record<string, unknown>): boolean {
|
|||||||
name: String(u?.name || u?.nickName || ''),
|
name: String(u?.name || u?.nickName || ''),
|
||||||
id: String(u?.userId ?? ''),
|
id: String(u?.userId ?? ''),
|
||||||
})
|
})
|
||||||
const pollClassId = resolveTeacherScreenSharePollClassId(msg)
|
|
||||||
if (pollClassId) startTeacherScreenSharePolling(pollClassId)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopTeacherScreenShareLive() {
|
function stopTeacherScreenShareLive() {
|
||||||
stopTeacherScreenSharePolling()
|
|
||||||
$XXL.stopTeacherShareScreen()
|
$XXL.stopTeacherShareScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -447,7 +404,6 @@ async function init() {
|
|||||||
function close() {
|
function close() {
|
||||||
isManualClose = true
|
isManualClose = true
|
||||||
stopHeartbeat()
|
stopHeartbeat()
|
||||||
stopTeacherScreenSharePolling()
|
|
||||||
stopClassMonitor()
|
stopClassMonitor()
|
||||||
clearReconnectTimer()
|
clearReconnectTimer()
|
||||||
stopVisibilityListener()
|
stopVisibilityListener()
|
||||||
|
|||||||
@ -109,6 +109,10 @@ export const $XXL = {
|
|||||||
packSync('startTeacherShareScreen', JSON.stringify(config)),
|
packSync('startTeacherShareScreen', JSON.stringify(config)),
|
||||||
/** 老师投屏结束 */
|
/** 老师投屏结束 */
|
||||||
stopTeacherShareScreen: () => packSync('stopTeacherShareScreen'),
|
stopTeacherShareScreen: () => packSync('stopTeacherShareScreen'),
|
||||||
|
/** 学生课堂监控投屏开始 */
|
||||||
|
startStudentShareScreen: () => packSync('startStudentShareScreen'),
|
||||||
|
/** 学生课堂监控投屏结束 */
|
||||||
|
stopStudentShareScreen: () => packSync('stopStudentShareScreen'),
|
||||||
// openApp: (config: anyObj) => pack('openApp', config),
|
// openApp: (config: anyObj) => pack('openApp', config),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user