(function () { const sessionId = window.location.pathname.split("/").pop(); const canvas = document.getElementById("screen"); const ctx = canvas.getContext("2d"); const addressBar = document.getElementById("address-bar"); const statusEl = document.getElementById("status"); const overlay = document.getElementById("overlay"); const overlayMsg = document.getElementById("overlay-msg"); let ws = null; let viewportWidth = 1024; let viewportHeight = 576; let scaleX = 1; let scaleY = 1; let pingTimer = null; let pendingFrame = null; let frameScheduled = false; let frameCount = 0; let lastFpsTime = performance.now(); const frameImage = new Image(); const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsUrl = `${wsProtocol}//${window.location.host}/ws/${sessionId}`; function setStatus(text) { statusEl.textContent = text; } function showOverlay(message) { overlayMsg.textContent = message; overlay.classList.remove("hidden"); } function mapCoords(clientX, clientY) { const rect = canvas.getBoundingClientRect(); const x = (clientX - rect.left) / scaleX; const y = (clientY - rect.top) / scaleY; return { x, y }; } function send(payload) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(payload)); } } function drawDecodedFrame() { if (canvas.width !== frameImage.width || canvas.height !== frameImage.height) { canvas.width = frameImage.width; canvas.height = frameImage.height; viewportWidth = frameImage.width; viewportHeight = frameImage.height; updateScale(); } ctx.drawImage(frameImage, 0, 0); frameCount += 1; const now = performance.now(); if (now - lastFpsTime >= 2000) { const fps = Math.round((frameCount * 1000) / (now - lastFpsTime)); setStatus(`已连接 · ${fps} fps`); frameCount = 0; lastFpsTime = now; } } function scheduleFrame(arrayBuffer) { pendingFrame = arrayBuffer; if (frameScheduled) { return; } frameScheduled = true; requestAnimationFrame(() => { frameScheduled = false; if (!pendingFrame) { return; } const blob = new Blob([pendingFrame], { type: "image/jpeg" }); pendingFrame = null; const url = URL.createObjectURL(blob); frameImage.onload = () => { URL.revokeObjectURL(url); drawDecodedFrame(); }; frameImage.src = url; }); } function updateScale() { const rect = canvas.getBoundingClientRect(); scaleX = rect.width / viewportWidth; scaleY = rect.height / viewportHeight; } function connect() { ws = new WebSocket(wsUrl); ws.binaryType = "arraybuffer"; ws.onopen = () => { setStatus("已连接"); pingTimer = setInterval(() => send({ type: "ping" }), 60000); }; ws.onmessage = (event) => { if (event.data instanceof ArrayBuffer) { scheduleFrame(event.data); return; } try { const msg = JSON.parse(event.data); if (msg.type === "init") { viewportWidth = msg.width; viewportHeight = msg.height; addressBar.value = msg.url || ""; updateScale(); } else if (msg.type === "url" || msg.type === "url_update") { addressBar.value = msg.url || ""; } else if (msg.type === "closed") { showOverlay("会话已结束"); ws.close(); } else if (msg.type === "error") { setStatus(msg.message); } } catch (_) { /* ignore */ } }; ws.onclose = () => { setStatus("已断开"); clearInterval(pingTimer); }; ws.onerror = () => { setStatus("连接错误"); }; } canvas.addEventListener("click", (e) => { canvas.focus(); const { x, y } = mapCoords(e.clientX, e.clientY); send({ action: "click", x, y, button: "left" }); }); canvas.addEventListener("dblclick", (e) => { e.preventDefault(); const { x, y } = mapCoords(e.clientX, e.clientY); send({ action: "dblclick", x, y }); }); canvas.addEventListener("wheel", (e) => { e.preventDefault(); send({ action: "wheel", deltaX: e.deltaX, deltaY: e.deltaY }); }, { passive: false }); const specialKeys = new Set([ "Enter", "Backspace", "Delete", "Tab", "Escape", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown", ]); canvas.addEventListener("keydown", (e) => { e.preventDefault(); if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) { send({ action: "type", text: e.key }); } else if (specialKeys.has(e.key)) { send({ action: "press", key: e.key }); } else { send({ action: "keydown", key: e.key }); } }); canvas.addEventListener("keyup", (e) => { e.preventDefault(); if (e.key.length > 1 || e.ctrlKey || e.metaKey || e.altKey) { send({ action: "keyup", key: e.key }); } }); window.addEventListener("resize", updateScale); async function apiPost(path) { const res = await fetch(path, { method: "POST", credentials: "include" }); if (res.status === 401) { window.location.href = "/"; return null; } return res.json(); } async function ensureAuth() { const res = await fetch("/api/auth/me", { credentials: "include" }); if (!res.ok) { window.location.href = "/"; return false; } return true; } document.getElementById("btn-back").addEventListener("click", async () => { const data = await apiPost(`/api/session/${sessionId}/back`); if (data) addressBar.value = data.url; }); document.getElementById("btn-forward").addEventListener("click", async () => { const data = await apiPost(`/api/session/${sessionId}/forward`); if (data) addressBar.value = data.url; }); document.getElementById("btn-reload").addEventListener("click", async () => { const data = await apiPost(`/api/session/${sessionId}/reload`); if (data) addressBar.value = data.url; }); function navigateTo(url) { send({ type: "navigate", url }); } document.getElementById("btn-go").addEventListener("click", () => { navigateTo(addressBar.value.trim()); }); addressBar.addEventListener("keydown", (e) => { if (e.key === "Enter") { navigateTo(addressBar.value.trim()); } }); document.getElementById("btn-close").addEventListener("click", async () => { await fetch(`/api/session/${sessionId}`, { method: "DELETE", credentials: "include" }); showOverlay("会话已关闭"); if (ws) ws.close(); }); ensureAuth().then((ok) => { if (ok) { connect(); canvas.focus(); } }); })();