diff --git a/apps/docs/sst-env.d.ts b/apps/docs/sst-env.d.ts index 002938bb..b6a7e906 100644 --- a/apps/docs/sst-env.d.ts +++ b/apps/docs/sst-env.d.ts @@ -2,57 +2,8 @@ /* tslint:disable */ /* eslint-disable */ /* deno-fmt-ignore-file */ + +/// + import "sst" -export {} -declare module "sst" { - export interface Resource { - "Api": { - "type": "sst.cloudflare.Worker" - "url": string - } - "Auth": { - "type": "sst.cloudflare.Worker" - "url": string - } - "AuthFingerprintKey": { - "type": "random.index/randomString.RandomString" - "value": string - } - "CloudflareAuthKV": { - "type": "sst.cloudflare.Kv" - } - "DiscordClientID": { - "type": "sst.sst.Secret" - "value": string - } - "DiscordClientSecret": { - "type": "sst.sst.Secret" - "value": string - } - "GithubClientID": { - "type": "sst.sst.Secret" - "value": string - } - "GithubClientSecret": { - "type": "sst.sst.Secret" - "value": string - } - "InstantAdminToken": { - "type": "sst.sst.Secret" - "value": string - } - "InstantAppId": { - "type": "sst.sst.Secret" - "value": string - } - "LoopsApiKey": { - "type": "sst.sst.Secret" - "value": string - } - "Urls": { - "api": string - "auth": string - "type": "sst.sst.Linkable" - } - } -} +export {} \ No newline at end of file diff --git a/apps/www/package.json b/apps/www/package.json index ba7c0b99..8585f712 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -35,10 +35,12 @@ "@builder.io/qwik": "^1.8.0", "@builder.io/qwik-city": "^1.8.0", "@builder.io/qwik-react": "0.5.0", + "@fontsource-variable/bricolage-grotesque": "^5.1.1", + "@fontsource-variable/mona-sans": "^5.0.1", "@modular-forms/qwik": "^0.29.0", "@nestri/input": "*", "@nestri/libmoq": "*", - "@nestri/sdk": "0.1.0-alpha.11", + "@nestri/sdk": "0.1.0-alpha.14", "@nestri/ui": "*", "@openauthjs/openauth": "^0.2.6", "@polar-sh/checkout": "^0.1.8", diff --git a/apps/www/public/fonts/BasementGrotesque-Black.otf b/apps/www/public/fonts/BasementGrotesque-Black.otf new file mode 100644 index 00000000..2e38d843 Binary files /dev/null and b/apps/www/public/fonts/BasementGrotesque-Black.otf differ diff --git a/apps/www/public/fonts/BasementGrotesque-Black.woff b/apps/www/public/fonts/BasementGrotesque-Black.woff new file mode 100644 index 00000000..3047f4da Binary files /dev/null and b/apps/www/public/fonts/BasementGrotesque-Black.woff differ diff --git a/apps/www/public/fonts/BasementGrotesque-Black.woff2 b/apps/www/public/fonts/BasementGrotesque-Black.woff2 new file mode 100644 index 00000000..6004cefc Binary files /dev/null and b/apps/www/public/fonts/BasementGrotesque-Black.woff2 differ diff --git a/apps/www/public/images/avatars/dathorse.png b/apps/www/public/images/avatars/dathorse.png new file mode 100644 index 00000000..6c637c91 Binary files /dev/null and b/apps/www/public/images/avatars/dathorse.png differ diff --git a/apps/www/public/images/avatars/janried.png b/apps/www/public/images/avatars/janried.png new file mode 100644 index 00000000..33a9c1e2 Binary files /dev/null and b/apps/www/public/images/avatars/janried.png differ diff --git a/apps/www/public/images/avatars/victor.png b/apps/www/public/images/avatars/victor.png new file mode 100644 index 00000000..6895ed3b Binary files /dev/null and b/apps/www/public/images/avatars/victor.png differ diff --git a/apps/www/public/images/avatars/wanjohi.png b/apps/www/public/images/avatars/wanjohi.png new file mode 100644 index 00000000..0a45ed26 Binary files /dev/null and b/apps/www/public/images/avatars/wanjohi.png differ diff --git a/apps/www/public/images/screenshots/doom.png b/apps/www/public/images/screenshots/doom.png new file mode 100644 index 00000000..dc16993e Binary files /dev/null and b/apps/www/public/images/screenshots/doom.png differ diff --git a/apps/www/public/images/screenshots/main-dark.png b/apps/www/public/images/screenshots/main-dark.png new file mode 100644 index 00000000..18bd7303 Binary files /dev/null and b/apps/www/public/images/screenshots/main-dark.png differ diff --git a/apps/www/public/images/screenshots/main-light.png b/apps/www/public/images/screenshots/main-light.png new file mode 100644 index 00000000..53bcbc26 Binary files /dev/null and b/apps/www/public/images/screenshots/main-light.png differ diff --git a/apps/www/public/images/screenshots/movie.avifs b/apps/www/public/images/screenshots/movie.avifs new file mode 100644 index 00000000..1e34a70c Binary files /dev/null and b/apps/www/public/images/screenshots/movie.avifs differ diff --git a/apps/www/public/images/screenshots/multiplayer.png b/apps/www/public/images/screenshots/multiplayer.png new file mode 100644 index 00000000..067eb2ea Binary files /dev/null and b/apps/www/public/images/screenshots/multiplayer.png differ diff --git a/apps/www/src/root.tsx b/apps/www/src/root.tsx index f416f95d..9b8b2d32 100644 --- a/apps/www/src/root.tsx +++ b/apps/www/src/root.tsx @@ -27,6 +27,18 @@ export default component$(() => { + + {!isDev && ( t.data.length > 0 ? "Pro" : "Free").catch(async (err) => { + if (err instanceof Nestri.APIError) { + if (err.status == 404) { + return "Free" + } else { + throw err + } + } else { + throw err; + } + }) + + return subscriptions as "Free" | "Pro" + } + } +); + +export const getActiveUsers = server$( + async function () { + + const access = this.cookie.get("access_token") + if (access) { + const bearerToken = access.value + + try { + const nestriClient = new Nestri({ bearerToken, maxRetries: 5 }) + const users = await nestriClient.users.list().then(t => t.data) as any + return users as Nestri.Users.UserListResponse.Data[] + } catch (error) { + console.log("error", error) + return undefined + } + } + } +); + +export const getSession = server$( + async function (profileID: string) { + + const access = this.cookie.get("access_token") + if (access) { + const bearerToken = access.value + try { + const nestriClient = new Nestri({ bearerToken, maxRetries: 5 }) + const session = await nestriClient.users.session(profileID) + return session + } catch (error) { + console.log("error", error) + return undefined + } + } + } +); + +export const createSession = server$( + async function () { + const access = this.cookie.get("access_token") + if (access) { + const bearerToken = access.value + + try { + const nestriClient = new Nestri({ bearerToken, maxRetries: 5 }) + const taskID = await nestriClient.tasks.create() + const sessionID = await nestriClient.tasks.session(taskID.data) + return sessionID.data + } catch (error) { + console.log("error", error) + return undefined + } + } + } +); + +export default component$(() => { + + return ( +
+
+ { return await getUserSubscriptions() })} /> + { return await getActiveUsers() })} getSession$={$(async (profileID: string) => { return await getSession(profileID) })} /> + { return await getUserSubscriptions() })} createSession$={$(async () => { return await createSession() })} /> +
+
+ ) +}) \ No newline at end of file diff --git a/apps/www/src/routes/(play)/(user)/layout.tsx b/apps/www/src/routes/(play)/(user)/layout.tsx new file mode 100644 index 00000000..8c52ad63 --- /dev/null +++ b/apps/www/src/routes/(play)/(user)/layout.tsx @@ -0,0 +1,42 @@ +import Nestri from "@nestri/sdk"; +import { HomeNavBar } from "@nestri/ui"; +import { server$ } from "@builder.io/qwik-city"; +import { $, component$, Slot } from "@builder.io/qwik" + +const cookieOptions = { + path: "/", + sameSite: "lax" as const, + secure: false, // Only send cookies over HTTPS + //FIXME: This causes weird issues in Qwik + httpOnly: true, // Prevent JavaScript access to cookies + expires: new Date(Date.now() + 24 * 10 * 60 * 60 * 1000), // expires in like 10 days +} +export const getUserProfile = server$( + async function () { + const userData = this.cookie.get("profile_data") + if (userData) { + const user = userData.json() as Nestri.Users.UserRetrieveResponse.Data + return user + } else { + const access = this.cookie.get("access_token") + if (access) { + const bearerToken = access.value + + const nestriClient = new Nestri({ bearerToken, maxRetries: 5 }) + const currentProfile = await nestriClient.users.retrieve() + this.cookie.set("profile_data", JSON.stringify(currentProfile.data), cookieOptions) + return currentProfile.data; + } + } + } +); + +export default component$(() => { + + return ( + <> + { return await getUserProfile() })} /> + + + ) +}) \ No newline at end of file diff --git a/apps/www/src/routes/(play)/(user)/machine/index.tsx b/apps/www/src/routes/(play)/(user)/machine/index.tsx new file mode 100644 index 00000000..251ca8a3 --- /dev/null +++ b/apps/www/src/routes/(play)/(user)/machine/index.tsx @@ -0,0 +1,98 @@ +import { component$ } from "@builder.io/qwik" + +export default component$(() => { + + return ( +
+
+
+
+ + General + +
+
+ + Performance +
+
+ + Users +
+
+ + Security +
+
+ + Network + +
+
+ + API requests +
+
+ + Data transfer +
+
+ + WAN cache +
+
+ + Admin + +
+
+ + Settings +
+
+
+
+
+
+
+
+ +
+
+ No data yet +
+ Soon +
+
+ + Once you have installed a game and started playing data should start flowing into Nestri + +
+
+
+
+
+

GPU usage

+
+
+
+
+
+
+
+
+

CPU usage

+
+
+
+
+
+
+
+
+
+
+
+
+ ) +}) \ No newline at end of file diff --git a/apps/www/src/routes/(play)/[user]/index.tsx b/apps/www/src/routes/(play)/[user]/index.tsx deleted file mode 100644 index 217927a8..00000000 --- a/apps/www/src/routes/(play)/[user]/index.tsx +++ /dev/null @@ -1,490 +0,0 @@ -import { cn } from "@nestri/ui/design"; -import type Nestri from "@nestri/sdk"; -import { Modal } from "@qwik-ui/headless"; -import { Avatar, Icons } from "@nestri/ui"; -import { Link, routeLoader$ } from "@builder.io/qwik-city"; -import { HomeNavBar, SimpleFooter, GameStoreButton } from "@nestri/ui"; -import { component$, useSignal, useStore, useVisibleTask$ } from "@builder.io/qwik"; - -const games = [ - { - id: 2507950, - name: "Delta Force", - image: "https://assets-prd.ignimgs.com/2024/08/28/delta-force-button-replacement-1724855313566.jpg" - }, - { - id: 870780, - name: "Control Ultimate Edition", - image: "https://assets-prd.ignimgs.com/2023/04/08/sq-nswitchds-controlultimateeditioncloudversion-image500w-1680973421643.jpg" - }, - { - id: 1172470, - name: "Apex Legends", - image: "https://assets-prd.ignimgs.com/2023/02/16/apexrevelry-1676588335122.jpg" - }, - { - id: 914800, - name: "Coffee Talk", - image: "https://assets-prd.ignimgs.com/2022/11/09/coffee-talk-episode-1-button-fin-1668033710468.jpg" - }, { - id: 1085220, - name: "Figment 2: Creed Valley", - image: "https://assets-prd.ignimgs.com/2021/12/15/figment-2-button-1639602944843.jpg" - }, { - id: 1568400, - name: "Sheepy: A Short Adventure", - image: "https://assets-prd.ignimgs.com/2024/04/08/sheepy-1712557253260.jpg" - }, { - id: 271590, - name: "Grand Theft Auto V", - image: "https://assets-prd.ignimgs.com/2021/12/17/gta-5-button-2021-1639777058682.jpg" - }, { - id: 1086940, - name: "Baldur's Gate 3", - image: "https://assets-prd.ignimgs.com/2023/08/24/baldursg3-1692894717196.jpeg" - }, { - id: 1091500, - name: "Cyberpunk 2077", - image: "https://assets-prd.ignimgs.com/2020/07/16/cyberpunk-2077-button-fin-1594877291453.jpg" - }, { - id: 221100, - name: "DayZ", - image: "https://assets-prd.ignimgs.com/2021/12/20/dayz-1640044421966.jpg" - }, - -] - -export const useCurrentProfile = routeLoader$(async ({ sharedMap }) => { - const res = sharedMap.get("profile") as Nestri.Users.UserRetrieveResponse.Data | null - - return res - // return { - // avatarUrl: undefined, - // discriminator: 47, - // username: "WanjohiRyan" - // } -}) -//bg-blue-100 rounded-lg p-4 min-w-16 text-center -const TimeUnit = ({ value, label }: { value: number, label: string }) => ( -
-
- {new Array(2).fill(0).map((_, key) => { - const [digitOne, digitTwo] = value.toString().padStart(2, '0') - return ( -
-
-
9
-
0
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
0
-
-
- ) - })} -
-
{label}
-
-); - -const random = Math.floor(100 * Math.random()) - -export default component$(() => { - const profile = useCurrentProfile() - const isNewPerson = useSignal(false) - const targetDate = new Date('2025-01-29T23:59:00Z'); - - const timeLeft = useStore({ - days: 0, - hours: 0, - minutes: 0, - seconds: 0 - }) - - // eslint-disable-next-line qwik/no-use-visible-task - useVisibleTask$(() => { - isNewPerson.value = true - - const calculateTimeLeft = () => { - const difference = targetDate.getTime() - new Date().getTime(); - - if (difference > 0) { - const days = Math.floor(difference / (1000 * 60 * 60 * 24)); - const hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); - const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((difference % (1000 * 60)) / 1000); - - timeLeft.days = days - timeLeft.hours = hours - timeLeft.minutes = minutes - timeLeft.seconds = seconds - } - }; - - calculateTimeLeft(); - - const timer = setInterval(calculateTimeLeft, 1000); - - return () => clearInterval(timer); - }) - - return ( -
- {profile.value && } -
-
-
- - - - - - Add another Linux machine - - - -
-
-
- -
-
-
-

Add a Linux machine

-
- Download and install Nestri on your remote server or computer to connect it. Then paste the generated machine id here. -
-
-
- - -
-
-
-
-
-
-
-
-
-
- - - Find people to play with - -
-
    - {games.slice(5, 8).sort().map((game, key) => ( - - - {game.name} -
    - - {game.name} - -
    -
    - {new Array(3).fill(0).map((_, key) => ( -
    -
    '),url('data:image/svg+xml,')` - }} - > - -
    -
    - ))} -
    - -
    -
    -

    {`${(key + 1) * random} people are currently playing this game`}

    -
    -
    -
    - -
    -
    - - - -
    -
    - - -
    -
    -
    -
    -
    - -
    -
    -

    {game.name}

    -

    - A short handcrafted pixel art platformer that follows Sheepy, an abandoned plushy brought to life. Sheepy: A Short Adventure is the first short game from MrSuicideSheep. -

    -
    - - - -
    -
    -
    -

    Join a Nestri party

    -
    -
    -
    -
    -
    - ESRN-Teen -
    -
    -

    Teen [13+]

    - Mild Language, Violence, Blood and Gore, Drug References -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    - ))} -
    -
-
-
-
-
-
- - - Your Games - - -
-
    - {games.map((game, key) => ( - - - {game.name} -
    - {game.name} -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
      -
    • - - Shooter -
    • -
    • - - Action -
    • -
    • - - Free to play -
    • -
    -

    - Delta Force is a first-person shooter which offers players both a single player campaign based on the movie Black Hawk Down, but also large-scale PvP multiplayer action. The game was formerly known as Delta Force: Hawk Ops. -

    - -
    -
    -
    - - - ))} -
-
-
- - - -
-
- -
-
-
-
- - - - - - -
-
Nestri needs your help
-
- {profile.value && profile.value.avatarUrl ? (Avatar) : ()} - - {profile.value && profile.value.username} - -
-
-
-
-
What's wrong?
-
-
-
-
-
1
-
We're almost ready to launch Nestri, but server costs are our biggest hurdle right now.
-
-
-
2
-
As a bootstrapped startup (yeah, just a few passionate developers!), we're reaching out to our early believers.
-
-
-
3
-
Your early access subscription will directly fund our initial server infrastructure, helping us bring self-hosted cloud gaming to life.
-
-
-
-
-
- What you get -
-
-
-
- -
-
- Schedule 1-on-1 calls with the Founders -
-
-
-
- -
-
- Keep your special early supporter pricing forever -
-
-
-
- -
-
- Priority feature requests -
-
-
-
-
-
Full access in
-
- - - - -
-
-
-
- {/**https://sandbox-api.polar.sh/v1/checkout-links/polar_cl_3Kf9mOEl8We2ZnmYr0tolrFPfHiPvlC71XgZy4Jd2ni/redirect */} - Get early supporter price -
- -
-
-
-
-
-
-
-
- - - {/*
*/} -
- ) -}) \ No newline at end of file diff --git a/apps/www/src/routes/(play)/layout.tsx b/apps/www/src/routes/(play)/layout.tsx index ac31b8b1..8d91db09 100644 --- a/apps/www/src/routes/(play)/layout.tsx +++ b/apps/www/src/routes/(play)/layout.tsx @@ -1,14 +1,14 @@ -import type Nestri from "@nestri/sdk" +// import type Nestri from "@nestri/sdk" import { component$, Slot } from "@builder.io/qwik"; -import { type RequestHandler } from "@builder.io/qwik-city"; +// import { type RequestHandler } from "@builder.io/qwik-city"; -export const onRequest: RequestHandler = async ({ url, redirect, sharedMap }) => { - const currentProfile = sharedMap.get("profile") as Nestri.Users.UserRetrieveResponse.Data | null +// export const onRequest: RequestHandler = async ({ url, redirect, sharedMap }) => { +// // const currentProfile = sharedMap.get("profile") as Nestri.Users.UserRetrieveResponse.Data | null - if (!currentProfile) { - throw redirect(308, `${url.origin}`) - } -} +// // if (!currentProfile) { +// // throw redirect(308, `${url.origin}`) +// // } +// } export default component$(() => { return ( diff --git a/apps/www/src/routes/(play)/play/[id]/index.tsx b/apps/www/src/routes/(play)/play/[id]/index.tsx index 4352f4f0..ca859ded 100644 --- a/apps/www/src/routes/(play)/play/[id]/index.tsx +++ b/apps/www/src/routes/(play)/play/[id]/index.tsx @@ -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 + nestriKeyboard: NoSerialize + webrtc: NoSerialize + 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(); + const playState = useStore({ + 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 ( - { - // @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 ? ( +
+ + Offline + +
+ ) : ( + <> + < canvas + ref={canvas} + onClick$={lockPlay} + class="aspect-video h-full w-full object-contain max-h-screen" /> + {typeof playState.showOffline === "undefined" && ( +
+ +
+
+ {new Array(12).fill(0).map((i, k) => ( +
+ ))} +
+
+ Warming up the GPU... + +
+ )} + + )} + + +
+
+ + +
+
+
+
+ + +
+
+

Important information from Nestri

+
+ This product is in Alpha — please share feedback whenever possible to help us improve. Thanks you for your support! 💖 +
+
+ +
+
+
+ ) -}) - -{/** - .spinningCircleInner_b6db20 { - transform: rotate(280deg); - } - .inner_b6db20 { - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - contain: paint; - } */ -} - -{/* */ -} -// .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>, -// last_mouse_move: Arc>, // 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); -// } -// } +}) \ No newline at end of file diff --git a/apps/www/src/routes/(public)/blog/blog.css b/apps/www/src/routes/(public)/blog/blog.css index 1c36c112..a54ecce4 100644 --- a/apps/www/src/routes/(public)/blog/blog.css +++ b/apps/www/src/routes/(public)/blog/blog.css @@ -1,13 +1,13 @@ .blog h1 { - @apply text-4xl font-title font-bold text-gray-800 dark:text-gray-200; + @apply text-4xl font-bricolage font-bold text-gray-800 dark:text-gray-200; } .blog h2 { - @apply text-3xl font-title font-bold text-gray-800 dark:text-gray-200; + @apply text-3xl font-bricolage font-bold text-gray-800 dark:text-gray-200; } .blog h3 { - @apply text-2xl font-title font-bold text-gray-800 dark:text-gray-200; + @apply text-2xl font-mona font-bold text-gray-800 dark:text-gray-200; } .blog img { @@ -19,7 +19,7 @@ } .blog strong { - @apply text-gray-700 dark:text-gray-300 font-title font-semibold; + @apply text-gray-700 dark:text-gray-300 font-mona font-semibold; } .blog ul { diff --git a/apps/www/src/routes/(public)/blog/index.tsx b/apps/www/src/routes/(public)/blog/index.tsx index 0e0b18ba..8be9e7bd 100644 --- a/apps/www/src/routes/(public)/blog/index.tsx +++ b/apps/www/src/routes/(public)/blog/index.tsx @@ -30,7 +30,7 @@ export default component$(() => {
-

{blog.title}

+

{blog.title}

{blog.description}

diff --git a/apps/www/src/routes/(public)/blog/layout.tsx b/apps/www/src/routes/(public)/blog/layout.tsx index bdfb2b11..3e0a09a7 100644 --- a/apps/www/src/routes/(public)/blog/layout.tsx +++ b/apps/www/src/routes/(public)/blog/layout.tsx @@ -57,7 +57,7 @@ export default component$(() => {
-

+

{frontmatter.blogTitle}

diff --git a/apps/www/src/routes/(public)/fundraiser/index.tsx b/apps/www/src/routes/(public)/fundraiser/index.tsx index a4b4b70d..af9b890a 100644 --- a/apps/www/src/routes/(public)/fundraiser/index.tsx +++ b/apps/www/src/routes/(public)/fundraiser/index.tsx @@ -64,7 +64,7 @@ export default component$(() => {
-