initial commit to repository
This commit is contained in:
+309
@@ -0,0 +1,309 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CSS Stream</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/8.1.2/adapter.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
background: #000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
font-family: monospace;
|
||||
color: #fff;
|
||||
}
|
||||
#status {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
background: rgba(0,0,0,0.7);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
z-index: 10;
|
||||
}
|
||||
#hint {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0,0,0,0.7);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
z-index: 10;
|
||||
}
|
||||
#gameCanvas {
|
||||
width: 1280px;
|
||||
height: 720px;
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
object-fit: contain;
|
||||
cursor: none;
|
||||
display: block;
|
||||
}
|
||||
#faq {
|
||||
position: fixed;
|
||||
bottom: 40px;
|
||||
right: 10px;
|
||||
background: rgba(0,0,0,0.7);
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
max-width: 300px;
|
||||
font-size: 12px;
|
||||
z-index: 10;
|
||||
}
|
||||
#faq h2 { font-size: 13px; margin-bottom: 8px; color: #fff; }
|
||||
#faq h3 { font-size: 12px; color: #ffcc00; margin-bottom: 4px; }
|
||||
#faq p { color: #ccc; line-height: 1.4; margin-bottom: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="status">Connecting...</div>
|
||||
<video id="gameCanvas" autoplay muted playsinline></video>
|
||||
<div id="hint">Click to capture mouse | ESC to release</div>
|
||||
<div id="faq">
|
||||
<h2>FAQ</h2>
|
||||
<div class="faq-item">
|
||||
<h3>⚠️ Avoid pressing Ctrl+W while playing</h3>
|
||||
<p>Ctrl+W closes your browser tab and disconnects you from the game.</p>
|
||||
</div>
|
||||
<div class="faq-item">
|
||||
<h3>⚠️ Opening the in-game menu</h3>
|
||||
<p>Press ESC to release mouse control, then press ESC again to open the menu.</p>
|
||||
</div>
|
||||
<div class="faq-item">
|
||||
<h3>voice chat</h3>
|
||||
<p>Work in progress</p>
|
||||
</div>
|
||||
<div class="faq-item">
|
||||
<h3>Keybinds</h3>
|
||||
<p>v: crouch key currently</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
const SIG_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}`;
|
||||
|
||||
const statusEl = document.getElementById('status');
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
|
||||
let sigWs = null;
|
||||
let pc = null;
|
||||
let sessionId = null;
|
||||
let pointerLocked = false;
|
||||
|
||||
function setStatus(msg) {
|
||||
statusEl.textContent = msg;
|
||||
console.log('[client]', msg);
|
||||
}
|
||||
|
||||
// ── WebRTC ────────────────────────────────────────────────────────────
|
||||
async function setupPeerConnection() {
|
||||
pc = new RTCPeerConnection({
|
||||
iceServers: [{ urls: 'stun:stun.cloudflare.com:3478' }],
|
||||
bundlePolicy: 'max-bundle',
|
||||
});
|
||||
|
||||
const remoteStream = new MediaStream();
|
||||
canvas.srcObject = remoteStream;
|
||||
|
||||
pc.ontrack = event => {
|
||||
remoteStream.addTrack(event.track);
|
||||
if (event.track.kind === 'video') {
|
||||
setStatus('Streaming — click to play');
|
||||
}
|
||||
console.log(`[client] got ${event.track.kind} track`);
|
||||
};
|
||||
|
||||
await pc.setLocalDescription(await pc.createOffer({
|
||||
offerToReceiveVideo: true,
|
||||
offerToReceiveAudio: true,
|
||||
}));
|
||||
|
||||
sigWs.send(JSON.stringify({
|
||||
type: 'browser_connect',
|
||||
sdp: pc.localDescription.sdp,
|
||||
}));
|
||||
|
||||
setStatus('Waiting for session...');
|
||||
}
|
||||
|
||||
// ── Signaling ─────────────────────────────────────────────────────────
|
||||
function connectSignaling() {
|
||||
setStatus('Connecting...');
|
||||
sigWs = new WebSocket(SIG_URL);
|
||||
|
||||
sigWs.onopen = () => {
|
||||
setStatus('Connected — setting up stream...');
|
||||
setupPeerConnection();
|
||||
};
|
||||
|
||||
sigWs.onmessage = async e => {
|
||||
const msg = JSON.parse(e.data);
|
||||
|
||||
if (msg.type === 'session_created') {
|
||||
sessionId = msg.sessionId;
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(msg.sdpAnswer));
|
||||
await waitForIce();
|
||||
setStatus('ICE connected — pulling tracks...');
|
||||
sigWs.send(JSON.stringify({ type: 'pull_track', sessionId }));
|
||||
}
|
||||
else if (msg.type === 'track_ready') {
|
||||
const result = msg.trackResult;
|
||||
if (result.requiresImmediateRenegotiation) {
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(result.sessionDescription));
|
||||
await pc.setLocalDescription(await pc.createAnswer());
|
||||
sigWs.send(JSON.stringify({
|
||||
type: 'renegotiate',
|
||||
sessionId: sessionId,
|
||||
sdp: pc.localDescription.sdp,
|
||||
}));
|
||||
}
|
||||
}
|
||||
else if (msg.type === 'waiting') {
|
||||
setStatus('Waiting for game instance...');
|
||||
}
|
||||
else if (msg.type === 'error') {
|
||||
setStatus('Error: ' + msg.message);
|
||||
console.error('[client] server error:', msg.message);
|
||||
}
|
||||
};
|
||||
|
||||
sigWs.onerror = () => setStatus('Connection error');
|
||||
sigWs.onclose = () => setStatus('Disconnected');
|
||||
}
|
||||
|
||||
function waitForIce() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed') {
|
||||
resolve(); return;
|
||||
}
|
||||
const timeout = setTimeout(() => reject(new Error('ICE timeout')), 15000);
|
||||
pc.addEventListener('iceconnectionstatechange', () => {
|
||||
if (pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed') {
|
||||
clearTimeout(timeout); resolve();
|
||||
}
|
||||
if (pc.iceConnectionState === 'failed') {
|
||||
clearTimeout(timeout); reject(new Error('ICE failed'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Input ─────────────────────────────────────────────────────────────
|
||||
function sendInput(event) {
|
||||
if (!sigWs || sigWs.readyState !== WebSocket.OPEN || !sessionId) return;
|
||||
sigWs.send(JSON.stringify({ type: 'input', sessionId, event }));
|
||||
}
|
||||
|
||||
canvas.addEventListener('click', () => {
|
||||
canvas.muted = false;
|
||||
canvas.requestPointerLock();
|
||||
});
|
||||
|
||||
document.addEventListener('pointerlockchange', () => {
|
||||
pointerLocked = document.pointerLockElement === canvas;
|
||||
document.getElementById('hint').textContent = pointerLocked
|
||||
? 'Mouse captured — ESC to release'
|
||||
: 'Click to capture mouse | ESC to release';
|
||||
});
|
||||
|
||||
const heldKeys = new Set();
|
||||
|
||||
window.addEventListener('blur', () => {
|
||||
heldKeys.forEach(key => sendInput({ type: 'key_up', key }));
|
||||
heldKeys.clear();
|
||||
});
|
||||
|
||||
let lastMouseMove = 0;
|
||||
document.addEventListener('mousemove', e => {
|
||||
if (!pointerLocked) return;
|
||||
const now = performance.now();
|
||||
if (now - lastMouseMove < 16) return;
|
||||
lastMouseMove = now;
|
||||
sendInput({ type: 'mouse_move', dx: e.movementX, dy: e.movementY });
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousedown', e => {
|
||||
if (!pointerLocked) return;
|
||||
sendInput({ type: 'mouse_down', button: e.button + 1 });
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseup', e => {
|
||||
if (!pointerLocked) return;
|
||||
sendInput({ type: 'mouse_up', button: e.button + 1 });
|
||||
});
|
||||
|
||||
let lastMouseWheel = 0;
|
||||
canvas.addEventListener('wheel', e => {
|
||||
if (!pointerLocked) return;
|
||||
e.preventDefault();
|
||||
const now = performance.now();
|
||||
if (now - lastMouseWheel < 16) return;
|
||||
lastMouseWheel = now;
|
||||
sendInput({ type: 'mouse_wheel', direction: e.deltaY < 0 ? 'up' : 'down' });
|
||||
}, { passive: false });
|
||||
|
||||
const KEY_MAP = {
|
||||
'KeyW': 'w', 'KeyA': 'a', 'KeyS': 's', 'KeyD': 'd',
|
||||
'ArrowUp': 'Up', 'ArrowDown': 'Down', 'ArrowLeft': 'Left', 'ArrowRight': 'Right',
|
||||
'Space': 'space', 'ShiftLeft': 'shift', 'ShiftRight': 'shift',
|
||||
'ControlLeft': 'ctrl', 'ControlRight': 'ctrl',
|
||||
'AltLeft': 'alt', 'AltRight': 'alt',
|
||||
'Enter': 'Return', 'Escape': 'Escape', 'Tab': 'Tab',
|
||||
'Backspace': 'BackSpace', 'Delete': 'Delete',
|
||||
'KeyR': 'r', 'KeyF': 'f', 'KeyG': 'g', 'KeyE': 'e', 'KeyQ': 'q',
|
||||
'KeyZ': 'z', 'KeyX': 'x', 'KeyC': 'c', 'KeyV': 'v', 'KeyB': 'b',
|
||||
'KeyH': 'h', 'KeyI': 'i', 'KeyJ': 'j', 'KeyK': 'k', 'KeyL': 'l',
|
||||
'KeyM': 'm', 'KeyN': 'n', 'KeyO': 'o', 'KeyP': 'p',
|
||||
'KeyT': 't', 'KeyU': 'u', 'KeyY': 'y',
|
||||
'Digit1': '1', 'Digit2': '2', 'Digit3': '3', 'Digit4': '4', 'Digit5': '5',
|
||||
'Digit6': '6', 'Digit7': '7', 'Digit8': '8', 'Digit9': '9', 'Digit0': '0',
|
||||
'Minus': 'minus', 'Equal': 'equal', 'Slash': 'slash',
|
||||
'Period': 'period', 'Comma': 'comma', 'Backquote': 'grave',
|
||||
'BracketLeft': 'bracketleft', 'BracketRight': 'bracketright',
|
||||
'Backslash': 'backslash', 'CapsLock': 'Caps_Lock',
|
||||
'F1': 'F1', 'F2': 'F2', 'F3': 'F3', 'F4': 'F4',
|
||||
'F5': 'F5', 'F6': 'F6', 'F7': 'F7', 'F8': 'F8',
|
||||
'F9': 'F9', 'F10': 'F10', 'F11': 'F11', 'F12': 'F12',
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'F4' && e.altKey) { e.preventDefault(); return; }
|
||||
if (e.key === 'w' && e.ctrlKey) { e.preventDefault(); return; }
|
||||
if (e.key === 'q' && e.ctrlKey) { e.preventDefault(); return; }
|
||||
if (e.key === 'F11') { e.preventDefault(); return; }
|
||||
if (e.key === 'Escape') {
|
||||
sendInput({ type: 'key_down', key: 'Escape' });
|
||||
sendInput({ type: 'key_up', key: 'Escape' });
|
||||
return;
|
||||
}
|
||||
if (!pointerLocked) return;
|
||||
e.preventDefault();
|
||||
const key = KEY_MAP[e.code] || e.code.toLowerCase();
|
||||
heldKeys.add(key);
|
||||
sendInput({ type: 'key_down', key });
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', e => {
|
||||
if (e.key === 'Escape') {
|
||||
sendInput({ type: 'key_up', key: 'Escape' });
|
||||
return;
|
||||
}
|
||||
if (!pointerLocked) return;
|
||||
e.preventDefault();
|
||||
const key = KEY_MAP[e.code] || e.code.toLowerCase();
|
||||
heldKeys.delete(key);
|
||||
sendInput({ type: 'key_up', key });
|
||||
});
|
||||
|
||||
connectSignaling();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user