mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 16:55:37 +02:00
⭐ 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:
@@ -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);
|
||||
// }
|
||||
// }
|
||||
})
|
||||
Reference in New Issue
Block a user