import { reactive, markRaw } from 'vue'; import { SignalingClient } from '../services/SignalingClient'; import { WebRtcController } from '../services/WebRtcController'; import { loadProto } from '../proto/controlMessage'; // 与 Android/Flutter 端一致的 ICE 配置(请按需替换为自己的 TURN 凭据)。 export const DEFAULT_ICE_SERVERS = [ // 公共 TURN(relay 兜底):UDP + TCP 两种传输,TCP 用于 UDP 被防火墙拦截的网络。 { urls: 'turn:175.178.213.60:3478', username: 'fanhuitong', credential: 'Fan19961207..' }, { urls: 'turn:175.178.213.60:3478?transport=tcp', username: 'fanhuitong', credential: 'Fan19961207..' }, // 内网 TURN(与被控端同局域网时可用)。 // { urls: 'turn:192.168.100.224:3478', username: 'tt', credential: 'fht' }, // { urls: 'turn:192.168.100.224:3478?transport=tcp', username: 'tt', credential: 'fht' }, { urls: 'stun:175.178.213.60:3478' }, // { urls: 'stun:192.168.5.224:3478' }, // { urls: 'stun:stun.l.google.com:19302' }, // { urls: 'stun:stun1.l.google.com:19302' }, // { urls: 'stun:stun2.l.google.com:19302' }, ]; export const store = reactive({ serverUrl: 'ws://175.178.213.60:8088/ws/signal', deviceId: '', targetDeviceId: '', iceServers: DEFAULT_ICE_SERVERS, protoReady: false, signalingConnected: false, registered: false, rtcConnected: false, dataChannelOpen: false, statusText: '未连接', error: '', controlledDevices: [], stats: null, remoteStream: null, // 被控端上报的当前实际采集分辨率(宽/高/帧率),用于与分辨率下拉框保持一致。 currentResolution: null, }); let signaling = null; let webrtc = null; export async function initProto() { await loadProto(); store.protoReady = true; } function parsePayload(payload) { if (!payload) return {}; if (typeof payload === 'string') { try { return JSON.parse(payload); } catch { return {}; } } return payload; } export function connectSignaling() { store.error = ''; const deviceId = store.deviceId.trim(); if (!deviceId) { store.error = '请填写本机设备 ID'; return; } signaling = new SignalingClient({ serverUrl: store.serverUrl.trim(), deviceId, onConnected: () => { store.signalingConnected = true; store.statusText = '已连接信令服务器'; refreshDevices(); }, onDisconnected: () => { store.signalingConnected = false; store.registered = false; store.statusText = '已断开信令连接'; }, onError: (e) => { store.error = '信令错误: ' + e; }, onMessage: handleSignalMessage, }); signaling.connect(); } function handleSignalMessage(msg) { switch ((msg.type || '').toUpperCase()) { case 'REGISTER_SUCCESS': store.registered = true; store.statusText = '注册成功 (CONTROLLER)'; break; case 'DEVICE_LIST': { const list = msg.controlled || []; store.controlledDevices = Array.isArray(list) ? list : []; if (!store.controlledDevices.includes(store.targetDeviceId)) store.targetDeviceId = ''; 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() { signaling && signaling.requestDeviceList(); } 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 = '正在发起连接...'; if (webrtc) { 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; }, // 被控端上报当前实际采集分辨率,同步到 store 供 UI 展示/匹配预设。 onResolutionReport: (res) => { store.currentResolution = res; }, }); await webrtc.createOffer(authType, authValue); } catch (e) { // 捕获 createOffer 阶段的所有异常,避免未处理的 Promise 拒绝导致浏览器中断脚本执行、 // 间接影响 WebSocket 信令连接的存活。 console.error('[connectToDevice] 创建 Offer 失败:', e); store.error = '创建连接失败: ' + (e?.message || e); store.statusText = '连接失败'; if (webrtc) { await webrtc.close(); webrtc = null; } } } export async function disconnectDevice() { if (webrtc) { await webrtc.close(); webrtc = null; } store.rtcConnected = false; store.dataChannelOpen = false; store.remoteStream = null; store.stats = null; store.currentResolution = null; store.statusText = store.signalingConnected ? '已断开设备连接' : '未连接'; } export function disconnectSignaling() { disconnectDevice(); if (signaling) { signaling.disconnect(); signaling = null; } store.signalingConnected = 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); } // 请求被控端切换屏幕采集分辨率(width<=0 表示原生分辨率;height<=0 时按 width 长边依宽高比缩放) export function sendResolutionChange(width, height, fps) { return webrtc && webrtc.sendResolutionChange(width, height, fps); }