Files
cloud-browser/static/viewer.js
T

242 lines
6.6 KiB
JavaScript

(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();
}
});
})();