import { reactive, markRaw } from 'vue'; import { SignalingClient } from '../services/SignalingClient'; import { WebRtcController } from '../services/WebRtcController'; import { VideoRecorder } from '../services/VideoRecorder'; import { loadProto } from '../proto/controlMessage'; import * as api from '../services/ApiClient'; // 远程视频录制器(基于浏览器原生 MediaRecorder)。 const videoRecorder = new VideoRecorder(); // 默认 ICE 兜底(当服务端未返回 TURN 凭证时使用,仅 STUN,可能穿透失败)。 export const FALLBACK_ICE_SERVERS = [ { urls: 'stun:175.178.213.60:3478' }, { urls: 'stun:47.242.112.133:3478' }, ]; export const store = reactive({ serverUrl: 'wss://www.ttstd.com/signal', // 登录态 loggedIn: api.isLoggedIn(), username: '', loginError: '', loginBusy: false, // 本机设备 ID 由服务端 REGISTER_SUCCESS 下发,无需用户填写。 deviceId: '', targetDeviceId: '', iceServers: FALLBACK_ICE_SERVERS, protoReady: false, signalingConnected: false, signalingConnecting: false, registered: false, rtcConnected: false, dataChannelOpen: false, statusText: '未连接', error: '', controlledDevices: [], stats: null, remoteStream: null, currentResolution: null, recording: false, recordStatus: '', }); let signaling = null; let webrtc = null; export async function initProto() { await loadProto(); store.protoReady = true; } /* ---------------- 登录 / 登出 ---------------- */ export async function login(username, password) { store.loginBusy = true; store.loginError = ''; try { const data = await api.login(username, password); store.loggedIn = true; store.username = data.username || username; return true; } catch (e) { store.loginError = e.message || '登录失败'; return false; } finally { store.loginBusy = false; } } export async function register(username, password) { return api.register({ username, password }); } export async function logout() { await api.logout().catch(() => {}); store.loggedIn = false; store.username = ''; disconnectSignaling(); } /* ---------------- 信令连接(Bearer 由子协议携带) ---------------- */ export async function connectSignaling() { store.error = ''; if (!api.isLoggedIn()) { store.error = '请先登录'; return; } store.signalingConnecting = true; signaling = new SignalingClient({ serverUrl: store.serverUrl.trim(), onConnected: async () => { store.signalingConnected = true; store.signalingConnecting = false; store.statusText = '已连接信令服务器'; // 登录态下自动拉取一次 TURN 凭证与绑定设备列表。 await loadTurnCredentials(); await loadBindings(); }, onDisconnected: () => { store.signalingConnected = false; store.signalingConnecting = false; store.registered = false; store.statusText = '已断开信令连接'; }, onError: (e) => { store.signalingConnecting = false; store.error = '信令错误: ' + e; }, onMessage: handleSignalMessage, // 4001:尝试刷新令牌后重连。 onTokenExpired: async () => { try { await api.refresh(); reconnectSignaling(); } catch { await forceLogout('令牌已失效,请重新登录'); } }, // 4003:强制下线。 onForceLogout: async (reason) => { await forceLogout(reason || '账号已在其他位置登录'); }, }); signaling.connect(); } // 刷新令牌后重连(保留已登录态)。 function reconnectSignaling() { store.statusText = '令牌已刷新,正在重连...'; if (signaling) { signaling.disconnect(); signaling = null; } connectSignaling(); } async function forceLogout(reason) { store.error = reason; await api.logout().catch(() => {}); store.loggedIn = false; store.username = ''; disconnectSignaling(); } // 拉取 TURN 短期凭证(服务端开启时覆盖默认 ICE)。 export async function loadTurnCredentials() { const data = await api.fetchTurnCredentials(); if (data && Array.isArray(data.iceServers) && data.iceServers.length) { store.iceServers = data.iceServers; } else { store.iceServers = FALLBACK_ICE_SERVERS; } return store.iceServers; } // 拉取本机可连接的被控端(仅已绑定设备)。 export async function loadBindings() { try { const data = await api.listBindings(); const list = (data && data.bindings) || []; // 列表元素可能为 {deviceUid, alias, online} 或纯字符串。 store.controlledDevices = list.map((b) => typeof b === 'string' ? b : (b.deviceUid || b.deviceId || '') ).filter(Boolean); } catch { store.controlledDevices = []; } } function handleSignalMessage(msg) { switch ((msg.type || '').toUpperCase()) { case 'REGISTER_SUCCESS': store.registered = true; store.deviceId = msg.fromDeviceId || store.deviceId; store.statusText = '注册成功 (CONTROLLER)'; break; case 'DEVICE_LIST': { // 兜底:若服务端仍推送 DEVICE_LIST(兼容老逻辑)。 const list = msg.controlled || []; store.controlledDevices = Array.isArray(list) ? list : []; break; } case 'ANSWER': { store.statusText = '被控端已接受,正在建立连接...'; if (webrtc) { webrtc.handleAnswer(parsePayload(msg.payload).sdp).catch((e) => { store.error = '设置远端描述失败: ' + (e?.message || e); }); } break; } case 'ICE_CANDIDATE': webrtc && webrtc.handleIceCandidate(parsePayload(msg.payload)); break; case 'TARGET_OFFLINE': store.error = msg.payload || '目标被控端不在线,请确认设备已开启并连接服务器'; store.statusText = '连接失败'; break; case 'CONNECTION_REJECTED': case 'REQUEST_ERROR': case 'REQUEST_TIMEOUT': store.error = msg.payload || '连接请求失败'; store.statusText = '连接被拒绝'; break; } } export function refreshDevices() { loadBindings(); } export async function connectToDevice(targetId, authType = null, authValue = null) { if (!signaling || !store.signalingConnected) { store.error = '请先连接信令服务器'; return; } if (!targetId) { store.error = '请选择要控制的被控端设备'; return; } store.targetDeviceId = targetId; store.error = ''; store.statusText = '正在发起连接...'; store.recording = false; store.recordStatus = ''; if (webrtc) { if (videoRecorder.recording) await stopRecording(); await webrtc.close(); webrtc = null; } try { webrtc = new WebRtcController({ iceServers: store.iceServers, deviceId: store.deviceId, targetDeviceId: targetId, signaling, onIceState: (s) => { if (s === 'connected') { store.rtcConnected = true; store.statusText = '已连接,可远程控制'; } else if (s === 'disconnected' || s === 'failed') { store.rtcConnected = false; store.statusText = '连接已断开'; } }, onDataChannelState: (open) => { store.dataChannelOpen = open; }, onStream: (stream) => { store.remoteStream = markRaw(stream); }, onStats: (stats) => { store.stats = stats; }, onError: (msg) => { if (msg) store.error = msg; }, onResolutionReport: (res) => { store.currentResolution = res; }, }); await webrtc.createOffer(authType, authValue); } catch (e) { console.error('[connectToDevice] 创建 Offer 失败:', e); store.error = '创建连接失败: ' + (e?.message || e); store.statusText = '连接失败'; if (webrtc) { await webrtc.close(); webrtc = null; } } } export async function disconnectDevice() { if (videoRecorder.recording) { await stopRecording(); } if (webrtc) { await webrtc.close(); webrtc = null; } store.rtcConnected = false; store.dataChannelOpen = false; store.remoteStream = null; store.stats = null; store.currentResolution = null; store.recording = false; store.statusText = store.signalingConnected ? '已断开设备连接' : '未连接'; } export function toggleRecording() { if (store.recording) return stopRecording(); return startRecording(); } export function startRecording() { if (store.recording) return; if (!store.remoteStream) { store.error = '尚未接收到视频画面,无法录制'; return; } if (!videoRecorder.isSupported) { store.error = '当前浏览器不支持 MediaRecorder 录制'; return; } try { const ok = videoRecorder.start(store.remoteStream); if (ok) { store.recording = true; store.recordStatus = '录制中…'; } } catch (e) { store.recordStatus = ''; store.error = '开始录制失败: ' + (e?.message || e); } } export async function stopRecording() { const result = await videoRecorder.stop(); store.recording = false; if (result) { const a = document.createElement('a'); a.href = result.url; a.download = result.filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); store.recordStatus = '已保存:' + result.filename; setTimeout(() => URL.revokeObjectURL(result.url), 30000); } else { store.recordStatus = '录制已停止'; } return result; } export function disconnectSignaling() { disconnectDevice(); if (signaling) { signaling.disconnect(); signaling = null; } store.signalingConnected = false; store.signalingConnecting = false; store.registered = false; store.controlledDevices = []; } // 控制指令转发(供 UI 组件调用) export function sendTouch(x, y) { return webrtc && webrtc.sendTouch(x, y); } export function sendSwipe(x1, y1, x2, y2, duration) { return webrtc && webrtc.sendSwipe(x1, y1, x2, y2, duration); } export function sendLongPress(x, y) { return webrtc && webrtc.sendLongPress(x, y); } export function sendMotionEvent(action, x, y) { return webrtc && webrtc.sendMotionEvent(action, x, y); } export function sendKey(keyCode) { return webrtc && webrtc.sendKey(keyCode); } export function sendResolutionChange(width, height, fps) { return webrtc && webrtc.sendResolutionChange(width, height, fps); } function parsePayload(payload) { if (!payload) return {}; if (typeof payload === 'string') { try { return JSON.parse(payload); } catch { return {}; } } return payload; }