Files
VibeCoding/WebRTCControllerWeb/src/store/controllerStore.js

187 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = [
// 公共 TURNrelay 兜底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); }