diff --git a/src/components/HostListManagerDialog.vue b/src/components/HostListManagerDialog.vue new file mode 100644 index 0000000..6855a74 --- /dev/null +++ b/src/components/HostListManagerDialog.vue @@ -0,0 +1,381 @@ + + + + + \ No newline at end of file diff --git a/src/components/LeftToolbar.vue b/src/components/LeftToolbar.vue new file mode 100644 index 0000000..06c2353 --- /dev/null +++ b/src/components/LeftToolbar.vue @@ -0,0 +1,85 @@ + + + + + + \ No newline at end of file diff --git a/src/composables/useMonitor.js b/src/composables/useMonitor.js new file mode 100644 index 0000000..6239d98 --- /dev/null +++ b/src/composables/useMonitor.js @@ -0,0 +1,121 @@ +// src/composables/useMonitor.js +import { ref } from 'vue' + +/** + * 设备消息监测(含“有消息时暂停,此设备私信发送成功后恢复”) + * - 每 10s 轮询 getmesNum,但跳过 paused 的设备 + * - 暴露 pause/resume 方法,供外部在 onmessage 分支中调用 + * - 提供 openMonitor()/closeMonitor()/stop() 以适配你现有按钮逻辑 + * + * @param {object} options + * - deviceInformation: ref([]) 设备列表 + * - runType: ref('') 当前运行模式 + * - isStop: ref(false) 全局停止标志 + * - isMonitorOn: ref(false) 监测开关(UI 显示用) + * - isShowMes: ref() 定时器引用(外面已有) + * - wsActionsRef: () => wsActions 一个函数,返回你动态创建的 wsActions 对象 + * - onKickOnce?: (udid, index) => void 在恢复后,立刻补一次检测的 hook(默认调用 getmesNum) + */ +export function useMonitor({ + deviceInformation, + runType, + isStop, + isMonitorOn, + isShowMes, + wsActionsRef, + onKickOnce +}) { + // 用 UDID 做键,避免 index 变化错位 + const pausedDevices = new Set(); + + const isPausedByIndex = (index) => { + const udid = deviceInformation.value[index]?.udid; + return udid ? pausedDevices.has(udid) : false; + }; + + const pauseMonitorByIndex = (index) => { + const udid = deviceInformation.value[index]?.udid; + if (udid) pausedDevices.add(udid); + }; + + const resumeMonitorByIndex = (index) => { + const udid = deviceInformation.value[index]?.udid; + if (udid) pausedDevices.delete(udid); + }; + + const clearPaused = () => pausedDevices.clear(); + + const openMonitor = () => { + isStop.value = false; + // 立即跑一轮 + const wsActions = wsActionsRef?.(); + deviceInformation.value.forEach((device, index) => { + if (!isPausedByIndex(index)) { + wsActions?.getmesNum(device.udid, index); + } + }); + + runType.value = 'listen'; + isMonitorOn.value = true; + + // 每 10s 轮询未暂停设备 + isShowMes.value = setInterval(() => { + const ws = wsActionsRef?.(); + deviceInformation.value.forEach((device, index) => { + if (!isPausedByIndex(index)) { + ws?.getmesNum(device.udid, index); + } + }); + }, 10000); + }; + + const closeMonitor = () => { + isMonitorOn.value = false; + runType.value = ''; + if (isShowMes.value) { + clearInterval(isShowMes.value); + isShowMes.value = ''; + } + clearPaused(); + }; + + // 兼容你原来的 stop() 会顺手关监测 + const stopAll = () => { + closeMonitor(); + clearPaused(); + isStop.value = true; + }; + + // 用于“私信成功后立刻恢复并补一次检测” + const resumeAndKick = (index) => { + resumeMonitorByIndex(index); + setTimeout(() => { + const udid = deviceInformation.value[index]?.udid; + if (!udid) return; + + if (typeof onKickOnce === 'function') { + onKickOnce(udid, index); + } else { + const ws = wsActionsRef?.(); + ws?.getmesNum(udid, index); + } + }, 1500); + }; + + return { + // state + pausedDevices, + + // helpers + isPausedByIndex, + pauseMonitorByIndex, + resumeMonitorByIndex, + clearPaused, + + // controls + openMonitor, + closeMonitor, + stopAll, + resumeAndKick, + } +} diff --git a/src/composables/useStreams.js b/src/composables/useStreams.js new file mode 100644 index 0000000..db0a55f --- /dev/null +++ b/src/composables/useStreams.js @@ -0,0 +1,82 @@ +// composables/useStreams.js +import { nextTick } from 'vue' + +export function useStreams({ instanceList, videoElement, wslist, openStr, VideoConverter }) { + // 用 Map 做延迟队列,避免固定 8 个的限制 + const feedState = new Map() + + function ensureState(index) { + if (!feedState.has(index)) { + feedState.set(index, { processing: false, pending: null }) + } + return feedState.get(index) + } + + function pushFrame(index, buf) { + const st = ensureState(index) + if (st.processing) { + // 覆盖旧的等待帧,保留最新 + st.pending = buf + return + } + st.processing = true + try { + instanceList[index].converter.appendRawData(new Uint8Array(buf)) + } finally { + st.processing = false + if (st.pending) { + const next = st.pending + st.pending = null + queueMicrotask(() => pushFrame(index, next)) + } + } + } + + function resetFeedState(index) { + const st = feedState.get(index) + if (!st) return + st.processing = false + st.pending = null + } + + async function waitForVideoEl(udid, tries = 20, delay = 16) { + for (let i = 0; i < tries; i++) { + const el = videoElement.value?.[udid] + if (el) return el + await nextTick() + await new Promise(r => setTimeout(r, delay)) + } + return null + } + + function refreshStream(index, hard = true) { + const devices = Object.keys(videoElement.value || {}) + const udid = devices[index] + const video = videoElement.value?.[udid] + if (!video || !instanceList[index]) return + + resetFeedState(index) + try { instanceList[index].converter?.destroy?.() } catch { } + instanceList[index].converter = null + + if (hard) { + try { video.pause?.() } catch { } + try { video.removeAttribute?.('src') } catch { } + try { video.load?.() } catch { } + } + + // 重新挂新的 converter + instanceList[index].converter = new VideoConverter(video, 60, 1) + + // 让后端尽快推关键帧 + try { wslist[index]?.send?.(openStr) } catch { } + } + + return { + feedState, + pushFrame, + resetFeedState, + waitForVideoEl, + refreshStream, + } +} diff --git a/src/composables/useTeardown.js b/src/composables/useTeardown.js new file mode 100644 index 0000000..68a8d40 --- /dev/null +++ b/src/composables/useTeardown.js @@ -0,0 +1,108 @@ +// composables/useTeardown.js + +export function useTeardown(deps) { + let sseRef = null; + let multiplexWS = null; + + function setSSE(es) { + try { sseRef?.close?.(); } catch { } + sseRef = es; + } + + function setMultiplexWS(ws) { + try { multiplexWS?.close?.(); } catch { } + multiplexWS = ws; + } + + function disposeDevice(index) { + try { + const di = deps.deviceInformation.value[index]; + const udid = di?.udid; + + // 停止定时器 + if (deps.playTimer.value[index]) { + clearTimeout(deps.playTimer.value[index]); + deps.playTimer.value[index] = null; + } + const inst = deps.instanceList[index]; + if (inst?.timer) { + clearInterval(inst.timer); + inst.timer = null; + } + + // 关闭 WS + const ws = deps.wslist[index]; + if (ws) { + ws.onopen = ws.onmessage = ws.onerror = ws.onclose = null; + try { ws.close(); } catch { } + deps.wslist[index] = undefined; + } + if (udid) deps.wsCache.delete(udid); + + // 销毁解码器 + try { + inst?.converter?.destroy?.(); + } catch { } + if (deps.instanceList[index]) deps.instanceList[index].converter = undefined; + + // 释放