Files
VibeCoding/WebRTCControllerWeb/src/store/controllerStore.js
tongtongstudio 6eb2c7321a feat(controlled): 实现设备激活与安全认证流程
- 添加API客户端、加密存储和provision/token激活逻辑
- WebSocket改用Bearer令牌认证,移除REGISTER请求
- 设备ID改为服务端下发,支持令牌刷新和强制下线处理
- 新增deviceSecret加密存储和accessToken自动刷新
- 更新设备ID获取方式为出厂SN,添加安全存储依赖
2026-08-01 15:13:12 +08:00

337 lines
10 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 { 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;
}