feat: Update website, API, and infra (#164)

>Adds `maitred` in charge of handling automated game installs, updates,
and even execution.

>Not only that, we have the hosted stuff here
>- [x] AWS Task on ECS GPUs
>- [ ] Add a service to listen for game starts and stops
(docker-compose.yml)
>- [x] Add a queue for requesting a game to start
>- [x] Fix up the play/watch UI 

>TODO:
>- Add a README
>- Add an SST docs

Edit:

- This adds a new landing page, updates the homepage etc etc
>I forgot what the rest of the updated stuff are 😅
This commit is contained in:
Wanjohi
2025-02-11 12:26:35 +03:00
committed by GitHub
parent 93327bdf1a
commit 060718d8b0
139 changed files with 5814 additions and 5049 deletions

View File

@@ -1,328 +1,309 @@
// import posthog from "posthog-js";
import { Modal } from "@nestri/ui";
import { useLocation } from "@builder.io/qwik-city";
import { Keyboard, Mouse, WebRTCStream } from "@nestri/input"
import { component$, useSignal, useVisibleTask$ } from "@builder.io/qwik";
import { $, component$, noSerialize, type NoSerialize, useSignal, useStore, useVisibleTask$ } from "@builder.io/qwik";
// import Nestri from "@nestri/sdk";
//TODO: go full screen, then lock on "landscape" screen-orientation on mobile
// FIXME: Add authentication and authorization
// export const getUserSubscriptions = server$(
// async function () {
// const access = this.cookie.get("access_token")
// if (access) {
// const bearerToken = access.value
// const nestriClient = new Nestri({ bearerToken, maxRetries: 5 })
// const subscriptions = await nestriClient.users.session()
// return subscriptions as "Free" | "Pro"
// }
// }
// );
type PlayState = {
nestriMouse: NoSerialize<Mouse | undefined>
nestriKeyboard: NoSerialize<Keyboard | undefined>
webrtc: NoSerialize<WebRTCStream | undefined>
nestriLock?: boolean
hasStream?: boolean
showOffline?: boolean
video?: HTMLVideoElement
inputInitialized?: boolean
initializedVideo?: boolean
}
export default component$(() => {
const id = useLocation().params.id;
const showBannerModal = useSignal(false)
const showButtonModal = useSignal(false)
const canvas = useSignal<HTMLCanvasElement>();
const playState = useStore<PlayState>({
nestriMouse: undefined,
nestriKeyboard: undefined,
nestriLock: undefined,
webrtc: undefined,
video: undefined,
hasStream: undefined,
showOffline: undefined,
inputInitialized: false,
initializedVideo: false
})
const initializeInputDevices = $(() => {
if (!canvas.value || !playState.webrtc || playState.inputInitialized) return;
try {
playState.nestriMouse = noSerialize(new Mouse({
canvas: canvas.value,
webrtc: playState.webrtc
}));
playState.nestriKeyboard = noSerialize(new Keyboard({
canvas: canvas.value,
webrtc: playState.webrtc
}));
playState.inputInitialized = true;
console.log("Input devices initialized successfully");
} catch (error) {
console.error("Failed to initialize input devices:", error);
playState.inputInitialized = false;
}
});
const lockPlay = $(async () => {
if (!canvas.value || !playState.hasStream) return;
try {
await canvas.value.requestPointerLock();
await canvas.value.requestFullscreen();
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);
playState.nestriLock = true;
console.log("Keyboard lock acquired");
} catch (e) {
console.warn("Keyboard lock failed:", e);
playState.nestriLock = false;
}
}
}
} catch (error) {
console.error("Error during lock sequence:", error);
}
});
const setupPointerLockListener = $(() => {
document.addEventListener("pointerlockchange", () => {
if (!canvas.value) return;
if (document.pointerLockElement === canvas.value) {
// Initialize input devices when pointer is locked
if (!playState.inputInitialized) {
initializeInputDevices();
}
} else {
if (!showBannerModal.value) {
const playing = sessionStorage.getItem("showedBanner");
showBannerModal.value = !playing || playing !== "true";
showButtonModal.value = playing === "true";
}
// Clean up input devices
if (playState.nestriKeyboard) {
playState.nestriKeyboard.dispose();
playState.nestriKeyboard = undefined;
}
if (playState.nestriMouse) {
playState.nestriMouse.dispose();
playState.nestriMouse = undefined;
}
playState.nestriLock = undefined;
playState.inputInitialized = false;
}
});
});
const handleVideoInput = $(async () => {
if (!playState.video) return;
if (playState.initializedVideo) return;
await playState.video.play().then(() => {
if (canvas.value && playState.video) {
canvas.value.width = playState.video.videoWidth;
canvas.value.height = playState.video.videoHeight;
playState.initializedVideo = true
const ctx = canvas.value.getContext("2d");
const renderer = () => {
if (ctx && playState.hasStream && playState.video) {
ctx.drawImage(playState.video, 0, 0);
playState.video.requestVideoFrameCallback(renderer);
}
};
playState.video.requestVideoFrameCallback(renderer);
}
}).catch(error => {
console.error("Error playing video:", error);
});
});
// eslint-disable-next-line qwik/no-use-visible-task
useVisibleTask$(({ track }) => {
track(() => canvas.value);
if (!canvas.value) return; // Ensure canvas is available
// Create video element and make it output to canvas (TODO: improve this)
let video = document.getElementById("webrtc-video-player");
if (!video) {
video = document.createElement("video");
video.id = "stream-video-player";
video.style.visibility = "hidden";
const webrtc = new WebRTCStream("https://relay.dathorse.com", id, (mediaStream) => {
if (video && mediaStream && (video as HTMLVideoElement).srcObject === null) {
console.log("Setting mediastream");
(video as HTMLVideoElement).srcObject = mediaStream;
setupPointerLockListener();
try {
if (!playState.video) {
playState.video = document.createElement("video") as HTMLVideoElement
playState.video.style.visibility = "hidden";
playState.webrtc = noSerialize(new WebRTCStream("https://relay.dathorse.com", id, async (mediaStream) => {
if (playState.video && mediaStream && playState.video.srcObject === null) {
console.log("Setting mediastream");
playState.video.srcObject = mediaStream;
playState.hasStream = true;
playState.showOffline = false;
// @ts-ignore
window.hasstream = true;
// @ts-ignore
window.roomOfflineElement?.remove();
// @ts-ignore
window.playbtnelement?.remove();
const playing = sessionStorage.getItem("showedBanner")
if (!playing || playing != "true") {
if (!showBannerModal.value) showBannerModal.value = true
} else {
if (!showButtonModal.value) showButtonModal.value = true
}
const playbtn = document.createElement("button");
playbtn.style.position = "absolute";
playbtn.style.left = "50%";
playbtn.style.top = "50%";
playbtn.style.transform = "translateX(-50%) translateY(-50%)";
playbtn.style.width = "12rem";
playbtn.style.height = "6rem";
playbtn.style.borderRadius = "1rem";
playbtn.style.backgroundColor = "rgb(175, 50, 50)";
playbtn.style.color = "black";
playbtn.style.fontSize = "1.5em";
playbtn.textContent = "< Start >";
await handleVideoInput();
} else if (mediaStream === null) {
console.log("MediaStream is null, Room is offline");
playState.showOffline = true
playState.hasStream = false;
// Clear canvas if it has been set
if (canvas.value) {
const ctx = canvas.value.getContext("2d");
if (ctx) ctx.clearRect(0, 0, canvas.value.width, canvas.value.height);
}
} else if (playState.video && playState.video.srcObject !== null) {
console.log("Setting new mediastream");
playState.video.srcObject = mediaStream;
playState.hasStream = true;
playState.showOffline = true
playbtn.onclick = () => {
playbtn.remove();
(video as HTMLVideoElement).play().then(() => {
if (canvas.value) {
canvas.value.width = (video as HTMLVideoElement).videoWidth;
canvas.value.height = (video as HTMLVideoElement).videoHeight;
playState.video.play().then(() => {
// window.roomOfflineElement?.remove();
playState.showOffline = false
if (canvas.value && playState.video) {
canvas.value.width = playState.video.videoWidth;
canvas.value.height = playState.video.videoHeight;
const ctx = canvas.value.getContext("2d");
const renderer = () => {
// @ts-ignore
if (ctx && window.hasstream) {
ctx.drawImage((video as HTMLVideoElement), 0, 0);
(video as HTMLVideoElement).requestVideoFrameCallback(renderer);
if (ctx && playState.hasStream && playState.video) {
ctx.drawImage(playState.video, 0, 0);
playState.video.requestVideoFrameCallback(renderer);
}
}
(video as HTMLVideoElement).requestVideoFrameCallback(renderer);
playState.video.requestVideoFrameCallback(renderer);
}
});
document.addEventListener("pointerlockchange", () => {
if (!canvas.value) return; // Ensure canvas is available
// @ts-ignore
if (document.pointerLockElement && !window.nestrimouse && !window.nestrikeyboard) {
// @ts-ignore
window.nestrimouse = new Mouse({ canvas: canvas.value, webrtc });
// @ts-ignore
window.nestrikeyboard = new Keyboard({ canvas: canvas.value, webrtc });
// @ts-ignore
} else if (!document.pointerLockElement && window.nestrimouse && window.nestrikeyboard) {
// @ts-ignore
window.nestrimouse.dispose();
// @ts-ignore
window.nestrimouse = undefined;
// @ts-ignore
window.nestrikeyboard.dispose();
// @ts-ignore
window.nestrikeyboard = undefined;
// @ts-ignore
window.nestriLock = undefined;
}
});
};
document.body.append(playbtn);
// @ts-ignore
window.playbtnelement = playbtn;
} else if (mediaStream === null) {
console.log("MediaStream is null, Room is offline");
// @ts-ignore
window.playbtnelement?.remove();
// @ts-ignore
window.roomOfflineElement?.remove();
// Add a message to the screen
const offline = document.createElement("div");
offline.style.position = "absolute";
offline.style.left = "50%";
offline.style.top = "50%";
offline.style.transform = "translateX(-50%) translateY(-50%)";
offline.style.width = "auto";
offline.style.height = "auto";
offline.style.color = "lightgray";
offline.style.fontSize = "2em";
offline.textContent = "Offline";
document.body.append(offline);
// @ts-ignore
window.roomOfflineElement = offline;
// @ts-ignore
window.hasstream = false;
// Clear canvas if it has been set
if (canvas.value) {
const ctx = canvas.value.getContext("2d");
if (ctx) ctx.clearRect(0, 0, canvas.value.width, canvas.value.height);
}
} else if ((video as HTMLVideoElement).srcObject !== null) {
console.log("Setting new mediastream");
(video as HTMLVideoElement).srcObject = mediaStream;
// @ts-ignore
window.hasstream = true;
// Start video rendering
(video as HTMLVideoElement).play().then(() => {
// @ts-ignore
window.roomOfflineElement?.remove();
if (canvas.value) {
canvas.value.width = (video as HTMLVideoElement).videoWidth;
canvas.value.height = (video as HTMLVideoElement).videoHeight;
const ctx = canvas.value.getContext("2d");
const renderer = () => {
// @ts-ignore
if (ctx && window.hasstream) {
ctx.drawImage((video as HTMLVideoElement), 0, 0);
(video as HTMLVideoElement).requestVideoFrameCallback(renderer);
}
}
(video as HTMLVideoElement).requestVideoFrameCallback(renderer);
}
});
}
});
}));
}
} catch (error) {
console.log("error handling the media connection", error)
}
})
return (
<canvas
ref={canvas}
onClick$={async () => {
// @ts-ignore
if (canvas.value && window.hasstream && !window.nestriLock) {
// Do not use - unadjustedMovement: true - breaks input on linux
await canvas.value.requestPointerLock();
await canvas.value.requestFullscreen()
if (document.fullscreenElement !== null) {
// @ts-ignore
if ('keyboard' in window.navigator && 'lock' in window.navigator.keyboard) {
const keys = [
"AltLeft",
"AltRight",
"Tab",
"Escape",
"ContextMenu",
"MetaLeft",
"MetaRight"
];
console.log("requesting keyboard lock");
// @ts-ignore
window.navigator.keyboard.lock(keys).then(
() => {
console.log("keyboard lock success");
// @ts-ignore
window.nestriLock = true;
}
).catch(
(e: any) => {
console.log("keyboard lock failed: ", e);
// @ts-ignore
window.nestriLock = false;
}
)
} else {
console.log("keyboard lock not supported, navigator is: ", window.navigator, navigator);
// @ts-ignore
window.nestriLock = undefined;
}
}
}
}}
//TODO: go full screen, then lock on "landscape" screen-orientation on mobile
class="aspect-video h-full w-full object-contain max-h-screen" />
<>
{playState.showOffline ? (
<div class="w-screen h-screen flex justify-center items-center">
<span class="text-2xl font-semibold flex items-center gap-2" >
Offline
</span>
</div>
) : (
<>
< canvas
ref={canvas}
onClick$={lockPlay}
class="aspect-video h-full w-full object-contain max-h-screen" />
{typeof playState.showOffline === "undefined" && (
<div class="w-screen h-screen bg-gray-100 dark:bg-gray-900 absolute z-10 flex justify-center items-center">
<span class="text-xl font-semibold flex items-center gap-2" >
<div data-component="spinner">
<div>
{new Array(12).fill(0).map((i, k) => (
<div key={k} />
))}
</div>
</div>
Warming up the GPU...
</span>
</div>
)}
</>
)}
<Modal.Root bind:show={showButtonModal} closeOnBackdropClick={false}>
<Modal.Panel class="
dark:backdrop:bg-[#0009] backdrop:bg-[#b3b5b799] backdrop:backdrop-grayscale-[.3] w-full max-w-[370px] max-h-[75vh] rounded-xl border dark:border-[#343434] border-[#e2e2e2]
dark:[box-shadow:0_0_0_1px_rgba(255,255,255,0.08),_0_3.3px_2.7px_rgba(0,0,0,.1),0_8.3px_6.9px_rgba(0,0,0,.13),0_17px_14.2px_rgba(0,0,0,.17),0_35px_29.2px_rgba(0,0,0,.22),0px_-4px_4px_0px_rgba(0,0,0,.04)_inset] dark:bg-[#222b]
[box-shadow:0_0_0_1px_rgba(19,21,23,0.08),_0_3.3px_2.7px_rgba(0,0,0,.03),0_8.3px_6.9px_rgba(0,0,0,.04),0_17px_14.2px_rgba(0,0,0,.05),0_35px_29.2px_rgba(0,0,0,.06),0px_-4px_4px_0px_rgba(0,0,0,.07)_inset] bg-[#fffd]
backdrop-blur-lg py-4 px-5 modal" >
<div class="size-full flex flex-col">
<div class="flex flex-col gap-3" >
<button
onClick$={async () => {
showButtonModal.value = false;
await handleVideoInput()
await lockPlay();
}}
class="transition-all duration-200 focus:ring-2 focus:ring-gray-300 focus:dark:ring-gray-700 outline-none w-full hover:bg-gray-300 hover:dark:bg-gray-700 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 items-center justify-center font-medium font-title rounded-lg flex py-3 px-4" >
Continue Playing
</button>
<button class="transition-all duration-200 focus:ring-2 focus:ring-gray-300 focus:dark:ring-gray-700 outline-none w-full hover:bg-gray-300 hover:dark:bg-gray-700 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 items-center justify-center font-medium font-title rounded-lg flex py-3 px-4" >
Shutdown Nestri
</button>
</div>
</div>
</Modal.Panel>
</Modal.Root>
<Modal.Root bind:show={showBannerModal} closeOnBackdropClick={false}>
<Modal.Panel class="
dark:backdrop:bg-[#0009] backdrop:bg-[#b3b5b799] backdrop:backdrop-grayscale-[.3] w-full max-w-[37%] max-h-[75vh] rounded-xl border dark:border-[#343434] border-[#e2e2e2]
dark:[box-shadow:0_0_0_1px_rgba(255,255,255,0.08),_0_3.3px_2.7px_rgba(0,0,0,.1),0_8.3px_6.9px_rgba(0,0,0,.13),0_17px_14.2px_rgba(0,0,0,.17),0_35px_29.2px_rgba(0,0,0,.22),0px_-4px_4px_0px_rgba(0,0,0,.04)_inset] dark:bg-[#222b]
[box-shadow:0_0_0_1px_rgba(19,21,23,0.08),_0_3.3px_2.7px_rgba(0,0,0,.03),0_8.3px_6.9px_rgba(0,0,0,.04),0_17px_14.2px_rgba(0,0,0,.05),0_35px_29.2px_rgba(0,0,0,.06),0px_-4px_4px_0px_rgba(0,0,0,.07)_inset] bg-[#fffd]
backdrop-blur-lg py-4 px-5 modal" >
<div class="size-full flex flex-col">
<div class="dark:text-white text-black">
<h3 class="font-semibold text-2xl tracking-tight mb-2 font-title">Important information from Nestri</h3>
<div class="text-sm dark:text-white/[.79] text-[rgba(19,21,23,0.64)]" >
This product is in Alpha please share feedback whenever possible to help us improve. Thanks you for your support! 💖
</div>
</div>
<div class="sm:pt-10 sm:block hidden" >
<button
onClick$={async () => {
sessionStorage.setItem("showedBanner", "true");
showBannerModal.value = false;
await handleVideoInput()
await lockPlay();
}}
class="gap-3 outline-none hover:[box-shadow:0_0_0_2px_rgba(200,200,200,0.95),0_0_0_4px_#8f8f8f] dark:hover:[box-shadow:0_0_0_2px_#161616,0_0_0_4px_#707070] focus:[box-shadow:0_0_0_2px_rgba(200,200,200,0.95),0_0_0_4px_#8f8f8f] dark:focus:[box-shadow:0_0_0_2px_#161616,0_0_0_4px_#707070] [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] font-medium font-title rounded-lg flex h-[calc(2.25rem+2*1px)] flex-col text-white w-full leading-none truncate bg-primary-500 items-center justify-center" >
Continue
</button>
</div>
</div>
</Modal.Panel>
</Modal.Root>
</>
)
})
{/**
.spinningCircleInner_b6db20 {
transform: rotate(280deg);
}
.inner_b6db20 {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
contain: paint;
} */
}
{/* <div class="loadingPopout_a8c724" role="dialog" tabindex="-1" aria-modal="true"><div class="spinner_b6db20 spinningCircle_b6db20" role="img" aria-label="Loading"><div class="spinningCircleInner_b6db20 inner_b6db20"><svg class="circular_b6db20" viewBox="25 25 50 50"><circle class="path_b6db20 path3_b6db20" cx="50" cy="50" r="20"></circle><circle class="path_b6db20 path2_b6db20" cx="50" cy="50" r="20"></circle><circle class="path_b6db20" cx="50" cy="50" r="20"></circle></svg></div></div></div> */
}
// .loadingPopout_a8c724 {
// background-color: var(--background-secondary);
// display: flex;
// justify-content: center;
// padding: 8px;
// }
// .circular_b6db20 {
// animation: spinner-spinning-circle-rotate_b6db20 2s linear infinite;
// height: 100%;
// width: 100%;
// }
// 100% {
// transform: rotate(360deg);
// }
{/* .path3_b6db20 {
animation-delay: .23s;
stroke: var(--text-brand);
}
.path_b6db20 {
animation: spinner-spinning-circle-dash_b6db20 2s ease-in-out infinite;
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
fill: none;
stroke-width: 6;
stroke-miterlimit: 10;
stroke-linecap: round;
stroke: var(--brand-500);
}
circle[Attributes Style] {
cx: 50;
cy: 50;
r: 20;
}
user agent stylesheet
:not(svg) {
transform-origin: 0px 0px;
} */
}
// .path2_b6db20 {
// animation-delay: .15s;
// stroke: var(--text-brand);
// opacity: .6;
// }
// .path_b6db20 {
// animation: spinner-spinning-circle-dash_b6db20 2s ease-in-out infinite;
// stroke-dasharray: 1, 200;
// stroke-dashoffset: 0;
// fill: none;
// stroke-width: 6;
// stroke-miterlimit: 10;
// stroke-linecap: round;
// stroke: var(--brand-500);
// }
// circle[Attributes Style] {
// cx: 50;
// cy: 50;
// r: 20;
// function throttle(func, limit) {
// let inThrottle;
// return function(...args) {
// if (!inThrottle) {
// func.apply(this, args);
// inThrottle = true;
// setTimeout(() => inThrottle = false, limit);
// }
// }
// }
// // Use it like this:
// const throttledMouseMove = throttle((x, y) => {
// websocket.send(JSON.stringify({
// type: 'mousemove',
// x: x,
// y: y
// }));
// }, 16); // ~60fps
// use std::time::Instant;
// // Add these to your AppState
// struct AppState {
// pipeline: Arc<Mutex<gst::Pipeline>>,
// last_mouse_move: Arc<Mutex<(i32, i32, Instant)>>, // Add this
// }
// // Then in your MouseMove handler:
// InputMessage::MouseMove { x, y } => {
// let mut last_move = state.last_mouse_move.lock().unwrap();
// let now = Instant::now();
// // Only process if coordinates are different or enough time has passed
// if (last_move.0 != x || last_move.1 != y) &&
// (now.duration_since(last_move.2).as_millis() > 16) { // ~60fps
// println!("Mouse moved to x: {}, y: {}", x, y);
// let structure = gst::Structure::builder("MouseMoveRelative")
// .field("pointer_x", x as f64)
// .field("pointer_y", y as f64)
// .build();
// let event = gst::event::CustomUpstream::new(structure);
// pipeline.send_event(event);
// // Update last position and time
// *last_move = (x, y, now);
// }
// }
})