mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +02:00
chore: Fix up the directories
Adds a basic standalone "play site" that mimics current one in apps/www. This is so self-hosters don't need to host whole site, but can just use small version of it. Yet to test so marking as draft, not at home currently so may take some time. Also might be good idea to make Caddy-powered container out of this later? <!-- This is an auto-generated comment: release notes by coderabbit.ai --> - New Features - Introduces a standalone Play site with server output, accessible on 0.0.0.0:3000. - Streams video via WebRTC into a canvas with continuous frame rendering. - Fullscreen and pointer lock support with optional keyboard lock for navigation keys. - Room-based routing with offline and loading states. - Responsive 16:9 canvas and improved default layout styling. - Chores - Adds a multi-stage container build for efficient runtime images and a lightweight init process. - Includes configuration and project setup for the standalone package. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: DatCaptainHorse <datcaptainhorse@users.noreply.github.com>
This commit is contained in:
committed by
Wanjohi
parent
85d6fdd213
commit
dc6a53e18d
35
packages/play-standalone/src/layouts/DefaultLayout.astro
Normal file
35
packages/play-standalone/src/layouts/DefaultLayout.astro
Normal file
@@ -0,0 +1,35 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg"/>
|
||||
<meta name="viewport" content="width=device-width"/>
|
||||
<meta name="generator" content={Astro.generator}/>
|
||||
<title>Nestri Standalone Play</title>
|
||||
</head>
|
||||
<body>
|
||||
<slot></slot>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Basement Grotesque";
|
||||
src: url("/fonts/BasementGrotesque-Black.woff") format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: "Basement Grotesque", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #191919;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
136
packages/play-standalone/src/pages/[room].astro
Normal file
136
packages/play-standalone/src/pages/[room].astro
Normal file
@@ -0,0 +1,136 @@
|
||||
---
|
||||
import DefaultLayout from "../layouts/DefaultLayout.astro";
|
||||
const { room } = Astro.params;
|
||||
---
|
||||
|
||||
<DefaultLayout>
|
||||
<h1 id="offlineText" class="offline">Offline</h1>
|
||||
<h1 id="loadingText" class="loading">Warming up the GPU...</h1>
|
||||
<canvas id="playCanvas" class="playCanvas" data-room={room}></canvas>
|
||||
</DefaultLayout>
|
||||
|
||||
<script>
|
||||
import { Mouse, Keyboard, WebRTCStream } from "@nestri/input";
|
||||
|
||||
// Elements
|
||||
const canvas = document.getElementById("playCanvas")! as HTMLCanvasElement;
|
||||
const offlineText = document.getElementById("offlineText")! as HTMLHeadingElement;
|
||||
const loadingText = document.getElementById("loadingText")! as HTMLHeadingElement;
|
||||
|
||||
const room = canvas.dataset.room;
|
||||
if (!room || room.length <= 0) {
|
||||
throw new Error("Room parameter is required");
|
||||
}
|
||||
|
||||
offlineText.style.display = "flex";
|
||||
loadingText.style.display = "none";
|
||||
|
||||
// Get query parameter "peerURL" from the URL
|
||||
let peerURL = new URLSearchParams(window.location.search).get("peerURL");
|
||||
if (!peerURL || peerURL.length <= 0) {
|
||||
peerURL = "/dnsaddr/relay.dathorse.com/p2p/12D3KooWPK4v5wKYNYx9oXWjqLM8Xix6nm13o91j1Feqq98fLBsw";
|
||||
}
|
||||
|
||||
// Stream
|
||||
const stream = new WebRTCStream(peerURL, room, async (mediaStream) => {
|
||||
if (mediaStream && video.srcObject === null) {
|
||||
video.srcObject = mediaStream;
|
||||
offlineText.style.display = "none";
|
||||
loadingText.style.display = "flex";
|
||||
|
||||
await video.play().catch((e) => {
|
||||
console.error("Failed to play video:", e);
|
||||
});
|
||||
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const renderer = () => {
|
||||
if (ctx && video.srcObject) {
|
||||
ctx.drawImage(video, 0, 0);
|
||||
video.requestVideoFrameCallback(renderer);
|
||||
}
|
||||
};
|
||||
video.requestVideoFrameCallback(renderer);
|
||||
loadingText.style.display = "none";
|
||||
}
|
||||
});
|
||||
const video = document.createElement("video") as HTMLVideoElement;
|
||||
|
||||
// Input
|
||||
let nestriMouse: Mouse | null = null;
|
||||
let nestriKeyboard: Keyboard | null = null;
|
||||
|
||||
document.addEventListener("pointerlockchange", () => {
|
||||
if (document.pointerLockElement === canvas) {
|
||||
if (nestriMouse || nestriKeyboard)
|
||||
return;
|
||||
|
||||
nestriMouse = new Mouse({
|
||||
canvas: canvas,
|
||||
webrtc: stream,
|
||||
});
|
||||
nestriKeyboard = new Keyboard({
|
||||
canvas: canvas,
|
||||
webrtc: stream,
|
||||
});
|
||||
} else {
|
||||
if (nestriMouse) {
|
||||
nestriMouse.dispose();
|
||||
nestriMouse = null;
|
||||
}
|
||||
if (nestriKeyboard) {
|
||||
nestriKeyboard.dispose();
|
||||
nestriKeyboard = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const lockPlay = async function () {
|
||||
await canvas.requestFullscreen();
|
||||
await canvas.requestPointerLock();
|
||||
|
||||
if (document.fullscreenElement !== null) {
|
||||
if ("keyboard" in navigator && "lock" in (navigator.keyboard as any)) {
|
||||
const keys = [
|
||||
"AltLeft", "AltRight", "Tab", "Escape",
|
||||
"ContextMenu", "MetaLeft", "MetaRight"
|
||||
];
|
||||
|
||||
try {
|
||||
await (navigator.keyboard as any).lock(keys);
|
||||
console.log("Keyboard lock acquired");
|
||||
} catch (e) {
|
||||
console.warn("Keyboard lock failed:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
canvas.addEventListener("click", lockPlay);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.playCanvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100vh;
|
||||
object-fit: contain;
|
||||
aspect-ratio: 16 / 9;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.offline, .loading {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: lightgray;
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user