1244 lines
33 KiB
HTML
1244 lines
33 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Remote Godot Game</title>
|
|
|
|
<style>
|
|
html,
|
|
body {
|
|
margin: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
background: black;
|
|
}
|
|
|
|
canvas {
|
|
display: block;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
object-fit: contain;
|
|
background: black;
|
|
cursor: default;
|
|
}
|
|
|
|
canvas.locked {
|
|
cursor: none;
|
|
}
|
|
|
|
#status {
|
|
position: fixed;
|
|
top: 12px;
|
|
left: 12px;
|
|
z-index: 10;
|
|
padding: 8px 12px;
|
|
color: white;
|
|
background: rgba(0, 0, 0, 0.65);
|
|
font-family: monospace;
|
|
pointer-events: none;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div id="status">Connecting...</div>
|
|
|
|
<!-- Must match the Godot streamed frame resolution. -->
|
|
<canvas id="game" width="1366" height="768"></canvas>
|
|
|
|
<script>
|
|
"use strict";
|
|
|
|
// ----------------------------------------------------
|
|
// Configuration
|
|
// ----------------------------------------------------
|
|
|
|
const STREAM_WIDTH = 1366;
|
|
const STREAM_HEIGHT = 768;
|
|
|
|
const DEBUG_LOGGING = true;
|
|
|
|
function getSignalingUrl() {
|
|
const hostname = window.location.hostname;
|
|
|
|
const isLocalConnection =
|
|
window.location.protocol === "file:" ||
|
|
hostname === "" ||
|
|
hostname === "localhost" ||
|
|
hostname === "127.0.0.1" ||
|
|
hostname === "::1" ||
|
|
hostname.endsWith(".local") ||
|
|
hostname.startsWith("192.168.") ||
|
|
hostname.startsWith("10.") ||
|
|
hostname.startsWith("172.16.") ||
|
|
hostname.startsWith("172.17.") ||
|
|
hostname.startsWith("172.18.") ||
|
|
hostname.startsWith("172.19.") ||
|
|
hostname.startsWith("172.20.") ||
|
|
hostname.startsWith("172.21.") ||
|
|
hostname.startsWith("172.22.") ||
|
|
hostname.startsWith("172.23.") ||
|
|
hostname.startsWith("172.24.") ||
|
|
hostname.startsWith("172.25.") ||
|
|
hostname.startsWith("172.26.") ||
|
|
hostname.startsWith("172.27.") ||
|
|
hostname.startsWith("172.28.") ||
|
|
hostname.startsWith("172.29.") ||
|
|
hostname.startsWith("172.30.") ||
|
|
hostname.startsWith("172.31.");
|
|
|
|
if (isLocalConnection) {
|
|
return "ws://localhost:8081/browser";
|
|
}
|
|
|
|
return "wss://wstsa.astronand.dev/browser";
|
|
}
|
|
|
|
const SIGNALING_URL = getSignalingUrl();
|
|
|
|
// ----------------------------------------------------
|
|
// Logging
|
|
// ----------------------------------------------------
|
|
|
|
function log(...args) {
|
|
if (DEBUG_LOGGING) {
|
|
console.log("[StreamServer]", ...args);
|
|
}
|
|
}
|
|
|
|
function warn(...args) {
|
|
if (DEBUG_LOGGING) {
|
|
console.warn("[StreamServer]", ...args);
|
|
}
|
|
}
|
|
|
|
function errorLog(...args) {
|
|
console.error("[StreamServer]", ...args);
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// Canvas
|
|
// ----------------------------------------------------
|
|
|
|
const canvas = document.getElementById("game");
|
|
const statusElement = document.getElementById("status");
|
|
|
|
const context = canvas.getContext("2d", {
|
|
alpha: false,
|
|
desynchronized: true
|
|
});
|
|
|
|
const urlParams = new URLSearchParams(
|
|
window.location.search
|
|
);
|
|
|
|
let sessionId = urlParams.get("session_id") || "";
|
|
|
|
// ----------------------------------------------------
|
|
// Connection state
|
|
// ----------------------------------------------------
|
|
|
|
let signalingSocket = null;
|
|
let peerConnection = null;
|
|
let dataChannel = null;
|
|
|
|
let godotConnected = false;
|
|
let offerCreated = false;
|
|
|
|
let mouseLocked = false;
|
|
let serverMouseLocked = false;
|
|
|
|
let frameInProgress = false;
|
|
let currentImage = null;
|
|
|
|
const pressedKeys = new Set();
|
|
|
|
// ----------------------------------------------------
|
|
// Status
|
|
// ----------------------------------------------------
|
|
|
|
function setStatus(text) {
|
|
log("Status:", text);
|
|
statusElement.textContent = text;
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// Signaling
|
|
// ----------------------------------------------------
|
|
|
|
function sendSignalingMessage(message) {
|
|
if (
|
|
!signalingSocket ||
|
|
signalingSocket.readyState !== WebSocket.OPEN
|
|
) {
|
|
warn(
|
|
"Signaling socket is not open:",
|
|
message
|
|
);
|
|
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
signalingSocket.send(
|
|
JSON.stringify(message)
|
|
);
|
|
|
|
log("Signaling message sent:", message);
|
|
return true;
|
|
} catch (error) {
|
|
errorLog(
|
|
"Could not send signaling message:",
|
|
error
|
|
);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// Data channel
|
|
// ----------------------------------------------------
|
|
|
|
function sendEvent(event) {
|
|
if (
|
|
!dataChannel ||
|
|
dataChannel.readyState !== "open"
|
|
) {
|
|
warn(
|
|
"Data channel is not open:",
|
|
event
|
|
);
|
|
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
dataChannel.send(
|
|
JSON.stringify(event)
|
|
);
|
|
|
|
log("Input event sent:", event);
|
|
return true;
|
|
} catch (error) {
|
|
errorLog(
|
|
"Could not send input event:",
|
|
error
|
|
);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// Mouse locking
|
|
// ----------------------------------------------------
|
|
|
|
async function lockMouse() {
|
|
if (!serverMouseLocked) {
|
|
log(
|
|
"Godot has not enabled mouse locking"
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
if (!godotConnected) {
|
|
warn("Godot is not connected");
|
|
return;
|
|
}
|
|
|
|
if (
|
|
!dataChannel ||
|
|
dataChannel.readyState !== "open"
|
|
) {
|
|
warn("Input data channel is not open");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await canvas.requestPointerLock();
|
|
} catch (error) {
|
|
errorLog(
|
|
"Could not request pointer lock:",
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
function unlockMouse() {
|
|
if (
|
|
document.pointerLockElement === canvas
|
|
) {
|
|
document.exitPointerLock();
|
|
}
|
|
}
|
|
|
|
function updateMouseLockState() {
|
|
const locked =
|
|
document.pointerLockElement === canvas;
|
|
|
|
mouseLocked = locked;
|
|
|
|
canvas.classList.toggle(
|
|
"locked",
|
|
mouseLocked
|
|
);
|
|
|
|
if (mouseLocked) {
|
|
setStatus(
|
|
"Mouse locked — press Escape to unlock"
|
|
);
|
|
} else if (godotConnected) {
|
|
setStatus(
|
|
"Connected — click to lock mouse"
|
|
);
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// Keyboard conversion
|
|
// ----------------------------------------------------
|
|
|
|
function browserKeyToGodot(code) {
|
|
const keys = {
|
|
KeyA: 65,
|
|
KeyB: 66,
|
|
KeyC: 67,
|
|
KeyD: 68,
|
|
KeyE: 69,
|
|
KeyF: 70,
|
|
KeyG: 71,
|
|
KeyH: 72,
|
|
KeyI: 73,
|
|
KeyJ: 74,
|
|
KeyK: 75,
|
|
KeyL: 76,
|
|
KeyM: 77,
|
|
KeyN: 78,
|
|
KeyO: 79,
|
|
KeyP: 80,
|
|
KeyQ: 81,
|
|
KeyR: 82,
|
|
KeyS: 83,
|
|
KeyT: 84,
|
|
KeyU: 85,
|
|
KeyV: 86,
|
|
KeyW: 87,
|
|
KeyX: 88,
|
|
KeyY: 89,
|
|
KeyZ: 90,
|
|
|
|
Digit0: 48,
|
|
Digit1: 49,
|
|
Digit2: 50,
|
|
Digit3: 51,
|
|
Digit4: 52,
|
|
Digit5: 53,
|
|
Digit6: 54,
|
|
Digit7: 55,
|
|
Digit8: 56,
|
|
Digit9: 57,
|
|
|
|
Numpad0: 96,
|
|
Numpad1: 97,
|
|
Numpad2: 98,
|
|
Numpad3: 99,
|
|
Numpad4: 100,
|
|
Numpad5: 101,
|
|
Numpad6: 102,
|
|
Numpad7: 103,
|
|
Numpad8: 104,
|
|
Numpad9: 105,
|
|
|
|
Space: 32,
|
|
Enter: 4194309,
|
|
Tab: 4194308,
|
|
Backspace: 4194308,
|
|
Delete: 4194312,
|
|
Insert: 4194311,
|
|
Home: 4194313,
|
|
End: 4194315,
|
|
PageUp: 4194314,
|
|
PageDown: 4194316,
|
|
|
|
ShiftLeft: 4194325,
|
|
ShiftRight: 4194325,
|
|
|
|
ControlLeft: 4194326,
|
|
ControlRight: 4194326,
|
|
|
|
AltLeft: 4194327,
|
|
AltRight: 4194327,
|
|
|
|
MetaLeft: 4194328,
|
|
MetaRight: 4194328,
|
|
|
|
ArrowUp: 4194320,
|
|
ArrowDown: 4194322,
|
|
ArrowLeft: 4194319,
|
|
ArrowRight: 4194321,
|
|
|
|
Escape: 4194305,
|
|
|
|
F1: 4194332,
|
|
F2: 4194333,
|
|
F3: 4194334,
|
|
F4: 4194335,
|
|
F5: 4194336,
|
|
F6: 4194337,
|
|
F7: 4194338,
|
|
F8: 4194339,
|
|
F9: 4194340,
|
|
F10: 4194341,
|
|
F11: 4194342,
|
|
F12: 4194343,
|
|
|
|
Minus: 45,
|
|
Equal: 61,
|
|
BracketLeft: 91,
|
|
BracketRight: 93,
|
|
Backslash: 92,
|
|
Semicolon: 59,
|
|
Quote: 39,
|
|
Comma: 44,
|
|
Period: 46,
|
|
Slash: 47,
|
|
Backquote: 96
|
|
};
|
|
|
|
return keys[code] ?? 0;
|
|
}
|
|
|
|
function getCharacter(event) {
|
|
if (
|
|
typeof event.key === "string" &&
|
|
event.key.length === 1
|
|
) {
|
|
return event.key;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
function sendKeyEvent(event, pressed) {
|
|
const keycode = browserKeyToGodot(
|
|
event.code
|
|
);
|
|
|
|
if (keycode === 0) {
|
|
warn(
|
|
"Unknown browser key:",
|
|
event.code
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
sendEvent({
|
|
type: pressed
|
|
? "key_down"
|
|
: "key_up",
|
|
char: getCharacter(event),
|
|
keycode: keycode
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// Mouse input
|
|
// ----------------------------------------------------
|
|
|
|
function mouseButtonName(button) {
|
|
switch (button) {
|
|
case 0:
|
|
return "left";
|
|
|
|
case 1:
|
|
return "middle";
|
|
|
|
case 2:
|
|
return "right";
|
|
|
|
case 3:
|
|
return "wheel_up";
|
|
|
|
case 4:
|
|
return "wheel_down";
|
|
|
|
default:
|
|
return "left";
|
|
}
|
|
}
|
|
|
|
function getCanvasPosition(event) {
|
|
const rect =
|
|
canvas.getBoundingClientRect();
|
|
|
|
const scaleX =
|
|
canvas.width / rect.width;
|
|
|
|
const scaleY =
|
|
canvas.height / rect.height;
|
|
|
|
return {
|
|
x: (event.clientX - rect.left) * scaleX,
|
|
y: (event.clientY - rect.top) * scaleY
|
|
};
|
|
}
|
|
|
|
function sendMouseClick(event, pressed) {
|
|
const position =
|
|
getCanvasPosition(event);
|
|
|
|
sendEvent({
|
|
type: "mouse_click",
|
|
button: mouseButtonName(event.button),
|
|
x: position.x,
|
|
y: position.y,
|
|
pressed: pressed
|
|
});
|
|
}
|
|
|
|
function sendMouseMove(event) {
|
|
const position =
|
|
getCanvasPosition(event);
|
|
|
|
sendEvent({
|
|
type: "mouse_move",
|
|
x: event.movementX,
|
|
y: event.movementY,
|
|
position_x: position.x,
|
|
position_y: position.y,
|
|
button_mask: event.buttons
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// Input setup
|
|
// ----------------------------------------------------
|
|
|
|
function setupInput() {
|
|
log("Setting up input listeners");
|
|
|
|
// Normal mouse movement is always sent.
|
|
// This allows Godot Control nodes to receive
|
|
// hover and motion events.
|
|
canvas.addEventListener(
|
|
"mousemove",
|
|
event => {
|
|
sendMouseMove(event);
|
|
}
|
|
);
|
|
|
|
// Normal mouse press.
|
|
canvas.addEventListener(
|
|
"mousedown",
|
|
event => {
|
|
log("Mouse down:", event.button);
|
|
|
|
sendMouseClick(event, true);
|
|
}
|
|
);
|
|
|
|
// Normal mouse release.
|
|
canvas.addEventListener(
|
|
"mouseup",
|
|
event => {
|
|
log("Mouse up:", event.button);
|
|
|
|
sendMouseClick(event, false);
|
|
}
|
|
);
|
|
|
|
// Mouse wheel.
|
|
canvas.addEventListener(
|
|
"wheel",
|
|
event => {
|
|
event.preventDefault();
|
|
|
|
const position =
|
|
getCanvasPosition(event);
|
|
|
|
const button =
|
|
event.deltaY < 0
|
|
? "wheel_up"
|
|
: "wheel_down";
|
|
|
|
sendEvent({
|
|
type: "mouse_click",
|
|
button: button,
|
|
x: position.x,
|
|
y: position.y,
|
|
pressed: true
|
|
});
|
|
|
|
sendEvent({
|
|
type: "mouse_click",
|
|
button: button,
|
|
x: position.x,
|
|
y: position.y,
|
|
pressed: false
|
|
});
|
|
},
|
|
{
|
|
passive: false
|
|
}
|
|
);
|
|
|
|
// Request browser pointer lock when Godot
|
|
// has enabled mouse locking.
|
|
canvas.addEventListener(
|
|
"click",
|
|
async () => {
|
|
if (!serverMouseLocked) {
|
|
return;
|
|
}
|
|
|
|
if (!mouseLocked) {
|
|
await lockMouse();
|
|
}
|
|
}
|
|
);
|
|
|
|
// Relative movement while pointer locked.
|
|
document.addEventListener(
|
|
"mousemove",
|
|
event => {
|
|
if (!mouseLocked) {
|
|
return;
|
|
}
|
|
|
|
sendEvent({
|
|
type: "mouse_move",
|
|
x: event.movementX,
|
|
y: event.movementY,
|
|
|
|
// The virtual cursor is centered
|
|
// while pointer locked.
|
|
position_x: canvas.width / 2,
|
|
position_y: canvas.height / 2,
|
|
|
|
button_mask: event.buttons
|
|
});
|
|
}
|
|
);
|
|
|
|
document.addEventListener(
|
|
"pointerlockchange",
|
|
() => {
|
|
log("Pointer lock changed");
|
|
updateMouseLockState();
|
|
}
|
|
);
|
|
|
|
document.addEventListener(
|
|
"pointerlockerror",
|
|
() => {
|
|
errorLog("Pointer lock error");
|
|
|
|
mouseLocked = false;
|
|
canvas.classList.remove("locked");
|
|
|
|
setStatus(
|
|
"Could not lock mouse"
|
|
);
|
|
}
|
|
);
|
|
|
|
// Keyboard input.
|
|
document.addEventListener(
|
|
"keydown",
|
|
event => {
|
|
log("Key down:", event.code);
|
|
|
|
if (event.code === "Escape") {
|
|
unlockMouse();
|
|
return;
|
|
}
|
|
|
|
if (pressedKeys.has(event.code)) {
|
|
return;
|
|
}
|
|
|
|
pressedKeys.add(event.code);
|
|
sendKeyEvent(event, true);
|
|
}
|
|
);
|
|
|
|
document.addEventListener(
|
|
"keyup",
|
|
event => {
|
|
log("Key up:", event.code);
|
|
|
|
if (event.code === "Escape") {
|
|
return;
|
|
}
|
|
|
|
pressedKeys.delete(event.code);
|
|
sendKeyEvent(event, false);
|
|
}
|
|
);
|
|
|
|
// Release keys and buttons if the browser loses focus.
|
|
window.addEventListener(
|
|
"blur",
|
|
() => {
|
|
log(
|
|
"Window lost focus; releasing input"
|
|
);
|
|
|
|
for (const code of pressedKeys) {
|
|
const keycode =
|
|
browserKeyToGodot(code);
|
|
|
|
if (keycode !== 0) {
|
|
sendEvent({
|
|
type: "key_up",
|
|
char: "",
|
|
keycode: keycode
|
|
});
|
|
}
|
|
}
|
|
|
|
pressedKeys.clear();
|
|
|
|
for (
|
|
const button of [
|
|
"left",
|
|
"middle",
|
|
"right"
|
|
]
|
|
) {
|
|
sendEvent({
|
|
type: "mouse_click",
|
|
button: button,
|
|
x: 0,
|
|
y: 0,
|
|
pressed: false
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
window.addEventListener(
|
|
"contextmenu",
|
|
event => {
|
|
event.preventDefault();
|
|
}
|
|
);
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// WebRTC
|
|
// ----------------------------------------------------
|
|
|
|
function setupPeerConnection() {
|
|
if (peerConnection) {
|
|
return;
|
|
}
|
|
|
|
log("Creating RTCPeerConnection");
|
|
|
|
peerConnection = new RTCPeerConnection({
|
|
iceServers: [
|
|
{
|
|
urls: [
|
|
"stun:stun.l.google.com:19302"
|
|
]
|
|
}
|
|
|
|
// Add your real TURN credentials here
|
|
// if direct WebRTC connections fail:
|
|
//
|
|
// {
|
|
// urls: "turn:turn.astronand.dev:3478",
|
|
// username: "YOUR_USERNAME",
|
|
// credential: "YOUR_PASSWORD"
|
|
// }
|
|
]
|
|
});
|
|
|
|
peerConnection.onicecandidate = event => {
|
|
if (!event.candidate) {
|
|
log("ICE gathering completed");
|
|
return;
|
|
}
|
|
|
|
sendSignalingMessage({
|
|
type: "ice",
|
|
candidate: event.candidate
|
|
});
|
|
};
|
|
|
|
peerConnection.onicegatheringstatechange = () => {
|
|
log(
|
|
"ICE gathering state:",
|
|
peerConnection.iceGatheringState
|
|
);
|
|
};
|
|
|
|
peerConnection.oniceconnectionstatechange = () => {
|
|
log(
|
|
"ICE connection state:",
|
|
peerConnection.iceConnectionState
|
|
);
|
|
};
|
|
|
|
peerConnection.onconnectionstatechange = () => {
|
|
const state =
|
|
peerConnection.connectionState;
|
|
|
|
log("WebRTC state:", state);
|
|
|
|
if (state === "connected") {
|
|
godotConnected = true;
|
|
|
|
setStatus(
|
|
"Connected — click to lock mouse"
|
|
);
|
|
}
|
|
|
|
if (
|
|
state === "failed" ||
|
|
state === "disconnected" ||
|
|
state === "closed"
|
|
) {
|
|
godotConnected = false;
|
|
serverMouseLocked = false;
|
|
|
|
unlockMouse();
|
|
|
|
setStatus(
|
|
"WebRTC connection lost"
|
|
);
|
|
}
|
|
};
|
|
|
|
dataChannel =
|
|
peerConnection.createDataChannel(
|
|
"input",
|
|
{
|
|
ordered: true
|
|
}
|
|
);
|
|
|
|
dataChannel.binaryType = "arraybuffer";
|
|
|
|
dataChannel.onopen = () => {
|
|
log("Input data channel opened");
|
|
|
|
godotConnected = true;
|
|
|
|
setStatus(
|
|
"Connected — click to lock mouse"
|
|
);
|
|
};
|
|
|
|
dataChannel.onclose = () => {
|
|
warn("Input data channel closed");
|
|
|
|
godotConnected = false;
|
|
serverMouseLocked = false;
|
|
|
|
unlockMouse();
|
|
|
|
setStatus(
|
|
"Data channel closed"
|
|
);
|
|
};
|
|
|
|
dataChannel.onerror = error => {
|
|
errorLog(
|
|
"Data channel error:",
|
|
error
|
|
);
|
|
};
|
|
|
|
dataChannel.onmessage = event => {
|
|
// Godot sends control messages as JSON text.
|
|
if (typeof event.data === "string") {
|
|
try {
|
|
const message =
|
|
JSON.parse(event.data);
|
|
|
|
log(
|
|
"Server message:",
|
|
message
|
|
);
|
|
|
|
if (
|
|
message.type ===
|
|
"mouse_locked"
|
|
) {
|
|
serverMouseLocked =
|
|
message.locked === true;
|
|
|
|
if (!serverMouseLocked) {
|
|
unlockMouse();
|
|
}
|
|
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
warn(
|
|
"Invalid server message:",
|
|
error
|
|
);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
receiveFrame(event.data);
|
|
};
|
|
}
|
|
|
|
async function createOffer() {
|
|
if (offerCreated) {
|
|
return;
|
|
}
|
|
|
|
if (!peerConnection) {
|
|
setupPeerConnection();
|
|
}
|
|
|
|
offerCreated = true;
|
|
|
|
try {
|
|
const offer =
|
|
await peerConnection.createOffer();
|
|
|
|
await peerConnection.setLocalDescription(
|
|
offer
|
|
);
|
|
|
|
sendSignalingMessage({
|
|
type: "offer",
|
|
offer: peerConnection.localDescription
|
|
});
|
|
|
|
setStatus(
|
|
"Connecting WebRTC..."
|
|
);
|
|
} catch (error) {
|
|
offerCreated = false;
|
|
|
|
errorLog(
|
|
"Could not create offer:",
|
|
error
|
|
);
|
|
|
|
setStatus(
|
|
"WebRTC offer failed"
|
|
);
|
|
}
|
|
}
|
|
|
|
async function handleAnswer(answer) {
|
|
if (!peerConnection) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await peerConnection.setRemoteDescription(
|
|
new RTCSessionDescription(answer)
|
|
);
|
|
} catch (error) {
|
|
errorLog(
|
|
"Could not set answer:",
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
async function handleOffer(offer) {
|
|
if (!offer) {
|
|
return;
|
|
}
|
|
|
|
setupPeerConnection();
|
|
|
|
try {
|
|
await peerConnection.setRemoteDescription(
|
|
new RTCSessionDescription(offer)
|
|
);
|
|
|
|
const answer =
|
|
await peerConnection.createAnswer();
|
|
|
|
await peerConnection.setLocalDescription(
|
|
answer
|
|
);
|
|
|
|
sendSignalingMessage({
|
|
type: "answer",
|
|
answer: peerConnection.localDescription
|
|
});
|
|
} catch (error) {
|
|
errorLog(
|
|
"Could not handle offer:",
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
async function handleIceCandidate(candidate) {
|
|
if (!candidate || !peerConnection) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await peerConnection.addIceCandidate(
|
|
candidate
|
|
);
|
|
} catch (error) {
|
|
errorLog(
|
|
"Could not add ICE candidate:",
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// Frame receiving
|
|
// ----------------------------------------------------
|
|
|
|
async function receiveFrame(data) {
|
|
if (frameInProgress) {
|
|
return;
|
|
}
|
|
|
|
frameInProgress = true;
|
|
|
|
try {
|
|
const blob = new Blob(
|
|
[data],
|
|
{
|
|
type: "image/jpeg"
|
|
}
|
|
);
|
|
|
|
const bitmap =
|
|
await createImageBitmap(blob);
|
|
|
|
if (currentImage) {
|
|
currentImage.close();
|
|
}
|
|
|
|
currentImage = bitmap;
|
|
|
|
context.drawImage(
|
|
bitmap,
|
|
0,
|
|
0,
|
|
STREAM_WIDTH,
|
|
STREAM_HEIGHT
|
|
);
|
|
} catch (error) {
|
|
errorLog(
|
|
"Frame decode error:",
|
|
error
|
|
);
|
|
} finally {
|
|
frameInProgress = false;
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// Signaling connection
|
|
// ----------------------------------------------------
|
|
|
|
function setupSignaling() {
|
|
log(
|
|
"Connecting to:",
|
|
SIGNALING_URL
|
|
);
|
|
|
|
signalingSocket =
|
|
new WebSocket(SIGNALING_URL);
|
|
|
|
signalingSocket.onopen = () => {
|
|
log("Signaling WebSocket opened");
|
|
|
|
setStatus(
|
|
"Waiting for Godot..."
|
|
);
|
|
|
|
const registration = {
|
|
type: "register",
|
|
role: "browser"
|
|
};
|
|
|
|
if (sessionId) {
|
|
registration.session_id =
|
|
sessionId;
|
|
}
|
|
|
|
sendSignalingMessage(
|
|
registration
|
|
);
|
|
};
|
|
|
|
signalingSocket.onmessage = async event => {
|
|
let message;
|
|
|
|
try {
|
|
message = JSON.parse(
|
|
event.data
|
|
);
|
|
} catch (error) {
|
|
errorLog(
|
|
"Invalid signaling message:",
|
|
event.data
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
log(
|
|
"Signaling message received:",
|
|
message
|
|
);
|
|
|
|
switch (message.type) {
|
|
case "session_created":
|
|
if (
|
|
!sessionId &&
|
|
message.session_id
|
|
) {
|
|
sessionId =
|
|
message.session_id;
|
|
}
|
|
|
|
break;
|
|
|
|
case "registered":
|
|
log(
|
|
"Browser registered"
|
|
);
|
|
|
|
break;
|
|
|
|
case "godot_connected":
|
|
godotConnected = true;
|
|
|
|
setStatus(
|
|
"Creating WebRTC connection..."
|
|
);
|
|
|
|
await createOffer();
|
|
|
|
break;
|
|
|
|
case "offer":
|
|
await handleOffer(
|
|
message.offer ||
|
|
message.sdp
|
|
);
|
|
|
|
break;
|
|
|
|
case "answer":
|
|
await handleAnswer(
|
|
message.answer ||
|
|
message.sdp
|
|
);
|
|
|
|
break;
|
|
|
|
case "ice":
|
|
case "candidate":
|
|
await handleIceCandidate(
|
|
message.candidate
|
|
);
|
|
|
|
break;
|
|
|
|
case "browser_disconnected":
|
|
case "godot_disconnected":
|
|
godotConnected = false;
|
|
serverMouseLocked = false;
|
|
|
|
unlockMouse();
|
|
|
|
setStatus(
|
|
"Godot disconnected"
|
|
);
|
|
|
|
break;
|
|
|
|
case "error":
|
|
errorLog(
|
|
"Signaling error:",
|
|
message.message
|
|
);
|
|
|
|
setStatus(
|
|
"Error: " +
|
|
message.message
|
|
);
|
|
|
|
break;
|
|
|
|
default:
|
|
warn(
|
|
"Unknown signaling message:",
|
|
message
|
|
);
|
|
|
|
break;
|
|
}
|
|
};
|
|
|
|
signalingSocket.onclose = event => {
|
|
warn(
|
|
"Signaling connection closed:",
|
|
event.code,
|
|
event.reason
|
|
);
|
|
|
|
godotConnected = false;
|
|
serverMouseLocked = false;
|
|
|
|
unlockMouse();
|
|
|
|
setStatus(
|
|
"Signaling disconnected"
|
|
);
|
|
};
|
|
|
|
signalingSocket.onerror = error => {
|
|
errorLog(
|
|
"Signaling error:",
|
|
error
|
|
);
|
|
|
|
setStatus(
|
|
"Signaling error"
|
|
);
|
|
};
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// Startup
|
|
// ----------------------------------------------------
|
|
|
|
log(
|
|
"Remote Godot client starting"
|
|
);
|
|
|
|
log(
|
|
"Canvas resolution:",
|
|
canvas.width,
|
|
"x",
|
|
canvas.height
|
|
);
|
|
|
|
setupInput();
|
|
setupSignaling();
|
|
</script>
|
|
</body>
|
|
</html>
|