mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-11 00:05:36 +02:00
⭐ feat(www): Finish up on the UI components (#158)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { Avatar } from "@nestri/ui";
|
||||
// import { } from "@qwik-ui/headless";
|
||||
import { component$ } from "@builder.io/qwik";
|
||||
import { HomeNavBar, SimpleFooter } from "@nestri/ui";
|
||||
import Avatar from "../../../../../packages/ui/src/avatar";
|
||||
import { HomeNavBar, Modal, SimpleFooter } from "@nestri/ui";
|
||||
import { cn } from "@nestri/ui/design";
|
||||
|
||||
const games = [
|
||||
{
|
||||
@@ -102,12 +104,38 @@ export default component$(() => {
|
||||
<img draggable={false} alt="game" width={256} height={256} src="https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/1623730/22a20bdaa6d782f60caa45eb7b02fc2411dcd988.ico" class=" h-12 bg-black ring-gray-400/70 ring-1 shadow-lg shadow-gray-900 w-12 translate-y-4 rotate-[14deg] rounded-lg object-cover transition-transform sm:h-16 sm:w-16 group-hover:scale-110" />
|
||||
</div>
|
||||
</button>
|
||||
<button class="border-gray-400/70 dark:border-gray-700/70 hover:ring-2 hover:ring-[#8f8f8f] dark:hover:ring-[#707070] group transition-all border-dashed duration-200 border-[2px] h-14 rounded-xl pl-4 gap-2 flex items-center justify-between overflow-hidden hover:bg-gray-300/70 dark:hover:bg-gray-700/70 outline-none disabled:opacity-50">
|
||||
<span class="py-2 text-gray-600/70 dark:text-gray-400/70 leading-none group-hover:text-black dark:group-hover:text-white shrink truncate flex text-start justify-center items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 size-5" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11.505 2h-1.501c-3.281 0-4.921 0-6.084.814a4.5 4.5 0 0 0-1.106 1.105C2 5.08 2 6.72 2 10s0 4.919.814 6.081a4.5 4.5 0 0 0 1.106 1.105C5.083 18 6.723 18 10.004 18h4.002c3.28 0 4.921 0 6.084-.814a4.5 4.5 0 0 0 1.105-1.105c.63-.897.772-2.08.805-4.081m-8-6h4m0 0h4m-4 0V2m0 4v4m-7 5h2m-1 3v4m-4 0h8" color="currentColor" /></svg>
|
||||
Add another Linux machine
|
||||
</span>
|
||||
</button>
|
||||
<Modal.Root class="w-full">
|
||||
<Modal.Trigger class="border-gray-400/70 w-full dark:border-gray-700/70 hover:ring-2 hover:ring-[#8f8f8f] dark:hover:ring-[#707070] group transition-all border-dashed duration-200 border-[2px] h-14 rounded-xl pl-4 gap-2 flex items-center justify-between overflow-hidden hover:bg-gray-300/70 dark:hover:bg-gray-700/70 outline-none disabled:opacity-50">
|
||||
<span class="py-2 text-gray-600/70 dark:text-gray-400/70 leading-none group-hover:text-black dark:group-hover:text-white shrink truncate flex text-start justify-center items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 size-5" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11.505 2h-1.501c-3.281 0-4.921 0-6.084.814a4.5 4.5 0 0 0-1.106 1.105C2 5.08 2 6.72 2 10s0 4.919.814 6.081a4.5 4.5 0 0 0 1.106 1.105C5.083 18 6.723 18 10.004 18h4.002c3.28 0 4.921 0 6.084-.814a4.5 4.5 0 0 0 1.105-1.105c.63-.897.772-2.08.805-4.081m-8-6h4m0 0h4m-4 0V2m0 4v4m-7 5h2m-1 3v4m-4 0h8" color="currentColor" /></svg>
|
||||
Add another Linux machine
|
||||
</span>
|
||||
</Modal.Trigger>
|
||||
<Modal.Panel class="
|
||||
dark:backdrop:bg-[#0009] backdrop:bg-[#b3b5b799] backdrop:backdrop-grayscale-[.3] w-[340px] 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 justify-between items-start ">
|
||||
<div class="mb-3 size-14 rounded-full text-[#939597] dark:text-[#d2d4d7] bg-[rgba(19,21,23,0.04)] dark:bg-white/[.08] flex items-center justify-center [&>svg]:size-8" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11.505 2h-1.501c-3.281 0-4.921 0-6.084.814a4.5 4.5 0 0 0-1.106 1.105C2 5.08 2 6.72 2 10s0 4.919.814 6.081a4.5 4.5 0 0 0 1.106 1.105C5.083 18 6.723 18 10.004 18h4.002c3.28 0 4.921 0 6.084-.814a4.5 4.5 0 0 0 1.105-1.105c.63-.897.772-2.08.805-4.081m-8-6h4m0 0h4m-4 0V2m0 4v4m-7 5h2m-1 3v4m-4 0h8" color="currentColor" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dark:text-white text-black">
|
||||
<h3 class="font-semibold text-2xl tracking-tight mb-2 font-title">Add a Linux machine</h3>
|
||||
<div class="text-sm dark:text-white/[.79] text-[rgba(19,21,23,0.64)]" >
|
||||
Download and install Nestri on your remote server or computer to connect it. Then paste the generated machine id here.
|
||||
</div>
|
||||
</div>
|
||||
<form action="#" class="mt-3 flex flex-col gap-3" >
|
||||
<input placeholder="fc27f428f9ca47d4b41b707ae0c62090" class="transition-all duration-200 w-full px-2 py-3 h-10 border text-black dark:text-white dark:border-[#343434] border-[#e2e2e2] rounded-md text-sm outline-none bg-white dark:bg-[rgba(19,21,23,0.64)] leading-none [background-image:-webkit-linear-gradient(hsla(0,0%,100%,0),hsla(0,0%,100%,0))]
|
||||
focus:[box-shadow:0_0_0_2px_#fcfcfc,0_0_0_4px_#8f8f8f] dark:focus:[box-shadow:0_0_0_2px_#161616,0_0_0_4px_#707070]" />
|
||||
<button class="w-full h-[calc(2.25rem+2*1px)] transition-all duration-200 hover:[box-shadow:0_0_0_2px_#fcfcfc,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_#fcfcfc,0_0_0_4px_#8f8f8f] dark:focus:[box-shadow:0_0_0_2px_#161616,0_0_0_4px_#707070] outline-none bg-primary-500 text-white rounded-lg text-sm" >Add machine</button>
|
||||
</form>
|
||||
</div>
|
||||
</Modal.Panel>
|
||||
</Modal.Root>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gap-2 w-full flex-col flex">
|
||||
@@ -118,51 +146,121 @@ export default component$(() => {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 size-5" viewBox="0 0 20 20"><path fill="currentColor" d="M2.049 9.112a8.001 8.001 0 1 1 9.718 8.692a1.5 1.5 0 0 0-.206-1.865l-.01-.01q.244-.355.47-.837a9.3 9.3 0 0 0 .56-1.592H9.744q.17-.478.229-1h2.82A15 15 0 0 0 13 10c0-.883-.073-1.725-.206-2.5H7.206l-.05.315a4.5 4.5 0 0 0-.971-.263l.008-.052H3.46q-.112.291-.198.595c-.462.265-.873.61-1.213 1.017m9.973-4.204C11.407 3.59 10.657 3 10 3s-1.407.59-2.022 1.908A9.3 9.3 0 0 0 7.42 6.5h5.162a9.3 9.3 0 0 0-.56-1.592M6.389 6.5c.176-.743.407-1.422.683-2.015c.186-.399.401-.773.642-1.103A7.02 7.02 0 0 0 3.936 6.5zm9.675 7H13.61a10.5 10.5 0 0 1-.683 2.015a6.6 6.6 0 0 1-.642 1.103a7.02 7.02 0 0 0 3.778-3.118m-2.257-1h2.733c.297-.776.46-1.62.46-2.5s-.163-1.724-.46-2.5h-2.733c.126.788.193 1.63.193 2.5s-.067 1.712-.193 2.5m2.257-6a7.02 7.02 0 0 0-3.778-3.118c.241.33.456.704.642 1.103c.276.593.507 1.272.683 2.015zm-7.76 7.596a3.5 3.5 0 1 0-.707.707l2.55 2.55a.5.5 0 0 0 .707-.707zM8 12a2.5 2.5 0 1 1-5 0a2.5 2.5 0 0 1 5 0" /></svg>
|
||||
Find people to play with
|
||||
</span>
|
||||
<button class="sm:flex hidden gap-1 items-center [&>svg]:size-5 cursor-pointer hover:text-gray-800 dark:hover:text-gray-200 transition-all duration-200 outline-none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v14m-7-7h14" /></svg>
|
||||
<span>Create a party</span>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="list-none ml-4 relative w-[calc(100%-1rem)]">
|
||||
{games.slice(5, 8).sort().map((game, key) => (
|
||||
<button key={`find-${key}`} class="gap-3.5 text-left hover:bg-gray-300/70 dark:hover:bg-gray-700/70 hover:ring-2 hover:ring-[#8f8f8f] dark:hover:ring-[#707070] outline-none group rounded-lg px-3 [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] flex items-center w-full">
|
||||
<img height={52} width={52} draggable={false} class="[transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] group-hover:scale-105 group-hover:shadow-lg group-hover:shadow-gray-900 select-none rounded-lg aspect-square w-[80px]" src={game.image} alt={game.name} />
|
||||
<div class="w-full h-[100px] overflow-hidden border-b-2 border-gray-400/70 dark:border-gray-700/70 flex group-[:nth-last-child(2)]:border-none flex-col gap-2 justify-center">
|
||||
<span class="font-medium tracking-tighter text-gray-700 dark:text-gray-300 max-w-full text-lg font-title truncate leading-none">
|
||||
{game.name}
|
||||
</span>
|
||||
<div class="flex items-center px-2 gap-2 w-full">
|
||||
<div
|
||||
class="items-center flex"
|
||||
style={{
|
||||
"--size": "1.25rem",
|
||||
"--cutout-avatar-percentage-visible": 0.4,
|
||||
"--head-margin-percentage": 0.2
|
||||
}}>
|
||||
{new Array(3).fill(0).map((_, key) => (
|
||||
<div key={key} class="relative items-start flex ml-[calc(-1*(1-var(--cutout-avatar-percentage-visible)-var(--head-margin-percentage))*var(--size))]">
|
||||
<div
|
||||
class="[&>svg]:size-5"
|
||||
style={{
|
||||
maskSize: "100% 100%",
|
||||
maskRepeat: "no-repeat",
|
||||
maskPosition: "center",
|
||||
maskComposite: "subtract",
|
||||
maskImage: `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><circle r="0.5" cx="0.5" cy="0.5"/></svg>'),url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><circle r="0.6" cx="1.1" cy="0.5"/></svg>')`
|
||||
}}
|
||||
>
|
||||
<Avatar name={((key + 1) * Math.floor(100 * Math.random())).toString()} />
|
||||
<Modal.Root key={`find-${key}`} >
|
||||
<Modal.Trigger class="gap-3.5 text-left hover:bg-gray-300/70 dark:hover:bg-gray-700/70 hover:ring-2 hover:ring-[#8f8f8f] dark:hover:ring-[#707070] outline-none group rounded-lg px-3 [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] flex items-center w-full">
|
||||
<img height={52} width={52} draggable={false} class="[transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] group-hover:scale-105 group-hover:shadow-lg group-hover:shadow-gray-900 select-none rounded-lg aspect-square w-[80px]" src={game.image} alt={game.name} />
|
||||
<div class={cn("w-full h-[100px] overflow-hidden border-b-2 border-gray-400/70 dark:border-gray-700/70 flex flex-col gap-2 justify-center", key == 2 && "border-none")}>
|
||||
<span class="font-medium tracking-tighter text-gray-700 dark:text-gray-300 max-w-full text-lg font-title truncate leading-none">
|
||||
{game.name}
|
||||
</span>
|
||||
<div class="flex items-center px-2 gap-2 w-full">
|
||||
<div
|
||||
class="items-center flex"
|
||||
style={{
|
||||
"--size": "1.25rem",
|
||||
"--cutout-avatar-percentage-visible": 0.4,
|
||||
"--head-margin-percentage": 0.2
|
||||
}}>
|
||||
{new Array(3).fill(0).map((_, key) => (
|
||||
<div key={key} class="relative items-start flex ml-[calc(-1*(1-var(--cutout-avatar-percentage-visible)-var(--head-margin-percentage))*var(--size))]">
|
||||
<div
|
||||
class="[&>svg]:size-5"
|
||||
style={{
|
||||
maskSize: "100% 100%",
|
||||
maskRepeat: "no-repeat",
|
||||
maskPosition: "center",
|
||||
maskComposite: "subtract",
|
||||
maskImage: `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><circle r="0.5" cx="0.5" cy="0.5"/></svg>'),url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><circle r="0.6" cx="1.1" cy="0.5"/></svg>')`
|
||||
}}
|
||||
>
|
||||
<Avatar name={((key + 1) * Math.floor(100 * Math.random())).toString()} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div class="[&>svg]:size-[--size] ml-[calc(-1*(1-var(--cutout-avatar-percentage-visible)-var(--head-margin-percentage))*var(--size))] relative flex items-center justify-center">
|
||||
<Avatar name={(key * Math.floor(100 * Math.random())).toString()} />
|
||||
</div>
|
||||
</div>
|
||||
<p class="font-normal text-gray-600 dark:text-gray-400 text-sm w-full truncate">{`${Math.floor(Math.random() * 100)} people are currently playing this game`}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Trigger>
|
||||
<Modal.Panel class="modal-sheet [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none] dark:backdrop:bg-[#0009] backdrop:bg-[#b3b5b799] backdrop:backdrop-grayscale-[.3] rounded-xl border dark:border-[#343434] border-[#e2e2e2] right-2 top-0 mr-2 mt-2
|
||||
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">
|
||||
<div class=" min-h-[calc(100dvh-1rem)] h-[calc(100dvh-1rem)] w-[550px] relative " >
|
||||
<div class="sticky top-0 w-full z-10 backdrop-blur-lg dark:bg-[rgba(19,21,23,0.48)] dark:border-white/[.08] border-b py-2 px-3 min-h-12 gap-3 flex justify-between items-center" >
|
||||
<Modal.Close class="dark:text-white/[.64] text-[rgba(19,21,23,0.64)] [&>svg]:size-5 [&>svg]:scale-[1.2] hover:text-white dark:hover:text-[rgb(19,21,23)] py-1.5 px-2.5 rounded-lg transition-all duration-200 hover:bg-[rgba(19,21,23,0.64)] dark:hover:bg-white/[.64]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" fill-rule="evenodd"><path d="M24 0v24H0V0zM12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" /><path fill="currentColor" d="M6.293 6.293a1 1 0 0 1 1.414 0l5 5a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414-1.414L10.586 12L6.293 7.707a1 1 0 0 1 0-1.414m6 0a1 1 0 0 1 1.414 0l5 5a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414-1.414L16.586 12l-4.293-4.293a1 1 0 0 1 0-1.414" /></g></svg>
|
||||
</Modal.Close>
|
||||
<div class="gap-2 flex justify-between flex-1 items-center ">
|
||||
<div class="w-full flex items-center gap-2">
|
||||
<button class="dark:text-white/[.64] dark:bg-white/[.08] text-[rgba(19,21,23,0.64)] bg-[rgba(19,21,23,0.04)] font-medium py-1.5 px-2.5 rounded-lg flex items-center gap-1 transition-all duration-200 [&>svg]:size-5 text-sm hover:text-white dark:hover:text-[rgb(19,21,23)] hover:bg-[rgba(19,21,23,0.64)] dark:hover:bg-white/[.64]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2"><path d="M14 7c0-.932 0-1.398-.152-1.765a2 2 0 0 0-1.083-1.083C12.398 4 11.932 4 11 4H8c-1.886 0-2.828 0-3.414.586S4 6.114 4 8v3c0 .932 0 1.398.152 1.765a2 2 0 0 0 1.083 1.083C5.602 14 6.068 14 7 14" /><rect width="10" height="10" x="10" y="10" rx="2" /></g></svg>
|
||||
Copy link
|
||||
</button>
|
||||
<button class="dark:text-white/[.64] dark:bg-white/[.08] text-[rgba(19,21,23,0.64)] bg-[rgba(19,21,23,0.04)] font-medium py-1.5 px-2.5 rounded-lg flex items-center gap-1 transition-all duration-200 [&>svg]:size-5 text-sm hover:text-white dark:hover:text-[rgb(19,21,23)] hover:bg-[rgba(19,21,23,0.64)] dark:hover:bg-white/[.64]">
|
||||
Game page
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 18L18 6m0 0H9m9 0v9" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 pt-0 gap-6 flex flex-col text-white" >
|
||||
<div class="m-4 mb-2 relative flex items-center justify-center" >
|
||||
<img src={game.image} height={280} width={280} class="rounded-xl bg-white/[.08] aspect-square size-[280px]" />
|
||||
</div>
|
||||
<div class="flex gap-2 flex-col dark:text-white text-black" >
|
||||
<h1 class="text-3xl font-title font-bold tracking-tight leading-none" >{game.name}</h1>
|
||||
<p class="dark:text-gray-400 text-gray-600 [display:-webkit-box] max-w-full overflow-hidden [-webkit-line-clamp:3] [-webkit-box-orient:vertical]" >
|
||||
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.
|
||||
</p>
|
||||
<div class="gap-y-1 gap-x-2 flex-wrap flex " >
|
||||
<button class="[&>svg]:size-[14px] cursor-pointer hover:border-primary-500 hover:text-primary-500 border-2 border-[rgba(19,21,23,0.08)] dark:border-white/[.16] items-center inline-flex py-1 px-2 rounded-[100px] gap-0.5 text-[rgba(19,21,23,0.36)] dark:text-[hsla(0,0%,100%,.5)] text-[0.875rem] font-medium transition-all duration-200" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M7.238 2.634a.75.75 0 1 0-1.476-.268L5.283 5H3a.75.75 0 1 0 0 1.5h2.01l-.545 3H2A.75.75 0 1 0 2 11h2.192l-.43 2.366a.75.75 0 1 0 1.476.268L5.717 11h3.475l-.43 2.366a.75.75 0 1 0 1.476.268L10.717 11H13a.75.75 0 0 0 0-1.5h-2.01l.545-3H14A.75.75 0 0 0 14 5h-2.192l.43-2.366a.75.75 0 1 0-1.476-.268L10.283 5H6.808zM9.465 9.5l.545-3H6.535l-.545 3z" clip-rule="evenodd" /></svg>
|
||||
Adventure
|
||||
</button>
|
||||
<button class="[&>svg]:size-[14px] cursor-pointer hover:border-primary-500 hover:text-primary-500 border-2 border-[rgba(19,21,23,0.08)] dark:border-white/[.16] items-center inline-flex py-1 px-2 rounded-[100px] gap-0.5 text-[rgba(19,21,23,0.36)] dark:text-[hsla(0,0%,100%,.5)] text-[0.875rem] font-medium transition-all duration-200" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M7.238 2.634a.75.75 0 1 0-1.476-.268L5.283 5H3a.75.75 0 1 0 0 1.5h2.01l-.545 3H2A.75.75 0 1 0 2 11h2.192l-.43 2.366a.75.75 0 1 0 1.476.268L5.717 11h3.475l-.43 2.366a.75.75 0 1 0 1.476.268L10.717 11H13a.75.75 0 0 0 0-1.5h-2.01l.545-3H14A.75.75 0 0 0 14 5h-2.192l.43-2.366a.75.75 0 1 0-1.476-.268L10.283 5H6.808zM9.465 9.5l.545-3H6.535l-.545 3z" clip-rule="evenodd" /></svg>
|
||||
Free to play
|
||||
</button>
|
||||
<button class="[&>svg]:size-[14px] cursor-pointer hover:border-primary-500 hover:text-primary-500 border-2 border-[rgba(19,21,23,0.08)] dark:border-white/[.16] items-center inline-flex py-1 px-2 rounded-[100px] gap-0.5 text-[rgba(19,21,23,0.36)] dark:text-[hsla(0,0%,100%,.5)] text-[0.875rem] font-medium transition-all duration-200" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M7.238 2.634a.75.75 0 1 0-1.476-.268L5.283 5H3a.75.75 0 1 0 0 1.5h2.01l-.545 3H2A.75.75 0 1 0 2 11h2.192l-.43 2.366a.75.75 0 1 0 1.476.268L5.717 11h3.475l-.43 2.366a.75.75 0 1 0 1.476.268L10.717 11H13a.75.75 0 0 0 0-1.5h-2.01l.545-3H14A.75.75 0 0 0 14 5h-2.192l.43-2.366a.75.75 0 1 0-1.476-.268L10.283 5H6.808zM9.465 9.5l.545-3H6.535l-.545 3z" clip-rule="evenodd" /></svg>
|
||||
Indie
|
||||
</button>
|
||||
</div>
|
||||
<div class="py-3 px-4 overflow-hidden bg-white/[.08] dark:bg-white/[.04] border dark:border-[#343434] border-[#e2e2e2] rounded-xl [box-shadow:0_1px_4px_rgba(0,0,0,.1)] dark:[box-shadow:0_1px_4px_rgba(0,0,0,.15)]" >
|
||||
<div class="dark:bg-white/[.08] bg-[rgba(19,21,23,0.04)] mx-[calc(-1rem+1px)] my-[calc(-0.75rem+1px)] mb-3 py-[calc(0.5rem-1px)] px-[calc(1rem-1px)]" >
|
||||
<p class="text-sm text-[rgba(19,21,23,0.64)] dark:text-[hsla(0,0%,100%,.79)] font-medium font-title" >Join a Nestri party</p>
|
||||
</div>
|
||||
<div class="gap-3 flex flex-col">
|
||||
<div class=" border-b border-[rgba(19,21,23,0.08)] dark:border-white/[.08] gap-3 flex flex-col [margin:-0.5rem_-1rem_0.25rem] [padding:0.5rem_1rem_0.75rem] ">
|
||||
<div class="flex gap-3">
|
||||
<div class="size-7 mt-2 shrink-0 flex items-center justify-center" >
|
||||
<img alt="ESRN-Teen" width={40} height={40} src="https://oyster.ignimgs.com/mediawiki/apis.ign.com/ratings/b/bf/ESRB-ver2013_T.png?width=325" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium font-title" >Teen [13+]</p>
|
||||
<span class="mt-[1px] text-sm leading-none text-[rgba(19,21,23,0.64)] dark:text-[hsla(0,0%,100%,.79)]" >Mild Language, Violence, Blood and Gore, Drug References</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=" mb-1 dark:text-white w-full leading-none -my-1 py-1">
|
||||
<button class="gap-3 outline-none hover:[box-shadow:0_0_0_2px_#fcfcfc,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_#fcfcfc,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" >
|
||||
Join this party
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div class="[&>svg]:size-[--size] ml-[calc(-1*(1-var(--cutout-avatar-percentage-visible)-var(--head-margin-percentage))*var(--size))] relative flex items-center justify-center">
|
||||
<Avatar name={(key * Math.floor(100 * Math.random())).toString()} />
|
||||
</div>
|
||||
</div>
|
||||
<p class="font-normal text-gray-600 dark:text-gray-400 text-sm w-full truncate">{`${Math.floor(Math.random() * 100)} open parties you can join`}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</Modal.Panel>
|
||||
</Modal.Root>
|
||||
))}
|
||||
<div class="[border:1px_dashed_theme(colors.gray.300)] dark:[border:1px_dashed_theme(colors.gray.800)] [mask-image:linear-gradient(rgb(0,0,0)_0%,_rgb(0,0,0)_calc(100%-120px),_transparent_100%)] bottom-0 top-0 -left-[0.4625rem] absolute" />
|
||||
</ul>
|
||||
@@ -180,14 +278,66 @@ export default component$(() => {
|
||||
<span>Install a game</span>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="relative py-3 w-full grid sm:grid-cols-3 grid-cols-2 list-none after:pointer-events-none after:select-none after:w-full after:h-[120px] after:fixed after:z-10 after:backdrop-blur-[1px] after:bg-gradient-to-b after:from-transparent after:to-gray-100 dark:after:to-gray-900 after:[-webkit-mask-image:linear-gradient(to_top,theme(colors.gray.100)_25%,transparent)] dark:after:[-webkit-mask-image:linear-gradient(to_top,theme(colors.gray.900)_25%,transparent)] after:[-webkit-backdrop-filter:1px] after:left-0 after:-bottom-[1px]">
|
||||
<ul class="relative py-3 w-full grid sm:grid-cols-3 grid-cols-2 list-none after:pointer-events-none after:select-none after:w-full after:h-[120px] after:fixed after:z-10 after:backdrop-blur-[1px] after:bg-gradient-to-b after:from-transparent after:to-gray-100 dark:after:to-gray-900 after:[-webkit-mask-image:linear-gradient(to_top,theme(colors.gray.100)_25%,transparent)] dark:after:[-webkit-mask-image:linear-gradient(to_top,theme(colors.gray.900)_25%,transparent)] after:left-0 after:-bottom-[1px]">
|
||||
{games.map((game, key) => (
|
||||
<button class="hover:bg-gray-300/70 dark:hover:bg-gray-700/70 [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] px-2 py-2 rounded-[15px] hover:ring-2 hover:ring-[#8f8f8f] dark:hover:ring-[#707070] outline-none size-full group [&_*]:transition-all [&_*]:duration-150 flex flex-col gap-2" key={key}>
|
||||
<img draggable={false} alt={game.name} class="select-none [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] group-hover:scale-[1.01] group-hover:shadow-lg group-hover:shadow-gray-900 w-full rounded-xl aspect-square" src={game.image} height={90} width={90} />
|
||||
<div class="flex flex-col px-2 w-full">
|
||||
<span class="max-w-full truncate">{game.name}</span>
|
||||
</div>
|
||||
</button>
|
||||
<Modal.Root key={`game-${key}`} >
|
||||
<Modal.Trigger class="hover:bg-gray-300/70 dark:hover:bg-gray-700/70 [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] px-2 py-2 rounded-[15px] hover:ring-2 hover:ring-[#8f8f8f] dark:hover:ring-[#707070] outline-none size-full group [&_*]:transition-all [&_*]:duration-150 flex flex-col gap-2" key={key}>
|
||||
<img draggable={false} alt={game.name} class="select-none [transition:all_0.3s_cubic-bezier(0.4,0,0.2,1)] group-hover:scale-[1.01] group-hover:shadow-lg group-hover:shadow-gray-900 w-full rounded-xl aspect-square" src={game.image} height={90} width={90} />
|
||||
<div class="flex flex-col px-2 w-full">
|
||||
<span class="max-w-full truncate">{game.name}</span>
|
||||
</div>
|
||||
</Modal.Trigger>
|
||||
<Modal.Panel class="dark:backdrop:bg-[#0009] backdrop:bg-[#b3b5b799] backdrop:backdrop-grayscale-[.3] rounded-xl border dark:border-[#343434] border-gray-300/70
|
||||
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-[rgb(22,22,22)]
|
||||
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 modal" >
|
||||
<div class="flex flex-col min-w-[17rem] relative text-black/70 dark:text-white/70 w-full max-w-[41.8125rem] min-h-[min(90%,100%-3rem)]" >
|
||||
<div class="flex-1 relative w-full " >
|
||||
<div class="relative w-full pb-[56.25%] overflow-hidden after:z-[2] after:absolute after:inset-0 dark:after:[background:linear-gradient(40deg,rgb(22,22,22)_24.16%,rgba(6,10,23,0)_56.61%),linear-gradient(0deg,rgb(22,22,22)_3.91%,rgba(6,10,23,0)_69.26%)]" >
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `url(https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/${game.id}/library_hero.jpg)`
|
||||
}}
|
||||
class={cn("absolute inset-0 z-[1] [transition:opacity_300ms_ease-in-out] bg-cover bg-[center_top] bg-no-repeat")} />
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `url(${game.image})`
|
||||
}}
|
||||
class={cn("absolute inset-0 -z-[1] bg-cover bg-[center_top] bg-no-repeat blur-[4rem]")} />
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `url(https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/${game.id}/logo.png)`
|
||||
}}
|
||||
class="absolute dark:bottom-0 min-[600px]:left-10 left-4 bg-contain bg-[center_bottom] bg-no-repeat min-[600px]:max-w-[40%] w-full aspect-video z-[3] bottom-[20px] " />
|
||||
</div>
|
||||
<div class="min-[600px]:p-10 min-[600px]:pt-4 pt-4 px-4 pb-6" >
|
||||
<ul class="[&_svg]:-mt-[2px] xl:mb-3 min-[960px]:mb-2 min-[600px]:leading-5 mb-4 leading-[0.625rem] list-none flex w-full" >
|
||||
<li class="mr-2 flex gap-0.5 [&>svg]:size-[14px] items-center justify-center text-sm dark:bg-[rgb(65,65,65)] bg-[rgb(171,171,171)] px-2 py-1 w-max rounded-md" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M7.238 2.634a.75.75 0 1 0-1.476-.268L5.283 5H3a.75.75 0 1 0 0 1.5h2.01l-.545 3H2A.75.75 0 1 0 2 11h2.192l-.43 2.366a.75.75 0 1 0 1.476.268L5.717 11h3.475l-.43 2.366a.75.75 0 1 0 1.476.268L10.717 11H13a.75.75 0 0 0 0-1.5h-2.01l.545-3H14A.75.75 0 0 0 14 5h-2.192l.43-2.366a.75.75 0 1 0-1.476-.268L10.283 5H6.808zM9.465 9.5l.545-3H6.535l-.545 3z" clip-rule="evenodd" /></svg>
|
||||
Shooter
|
||||
</li>
|
||||
<li class="mr-2 flex gap-0.5 [&>svg]:size-[14px] items-center justify-center text-sm dark:bg-[rgb(65,65,65)] bg-[rgb(171,171,171)] px-2 py-1 w-max rounded-md" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M7.238 2.634a.75.75 0 1 0-1.476-.268L5.283 5H3a.75.75 0 1 0 0 1.5h2.01l-.545 3H2A.75.75 0 1 0 2 11h2.192l-.43 2.366a.75.75 0 1 0 1.476.268L5.717 11h3.475l-.43 2.366a.75.75 0 1 0 1.476.268L10.717 11H13a.75.75 0 0 0 0-1.5h-2.01l.545-3H14A.75.75 0 0 0 14 5h-2.192l.43-2.366a.75.75 0 1 0-1.476-.268L10.283 5H6.808zM9.465 9.5l.545-3H6.535l-.545 3z" clip-rule="evenodd" /></svg>
|
||||
Action
|
||||
</li>
|
||||
<li class="mr-2 flex gap-0.5 [&>svg]:size-[14px] items-center justify-center text-sm dark:bg-[rgb(65,65,65)] bg-[rgb(171,171,171)] px-2 py-1 w-max rounded-md" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M7.238 2.634a.75.75 0 1 0-1.476-.268L5.283 5H3a.75.75 0 1 0 0 1.5h2.01l-.545 3H2A.75.75 0 1 0 2 11h2.192l-.43 2.366a.75.75 0 1 0 1.476.268L5.717 11h3.475l-.43 2.366a.75.75 0 1 0 1.476.268L10.717 11H13a.75.75 0 0 0 0-1.5h-2.01l.545-3H14A.75.75 0 0 0 14 5h-2.192l.43-2.366a.75.75 0 1 0-1.476-.268L10.283 5H6.808zM9.465 9.5l.545-3H6.535l-.545 3z" clip-rule="evenodd" /></svg>
|
||||
Free to play
|
||||
</li>
|
||||
</ul>
|
||||
<p class="text-black/90 dark:text-white/90 tracking-tight" >
|
||||
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.
|
||||
</p>
|
||||
<div class="sm:pt-10 sm:block hidden" >
|
||||
<button class="gap-3 outline-none hover:[box-shadow:0_0_0_2px_#fcfcfc,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_#fcfcfc,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" >
|
||||
Play Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Panel>
|
||||
</Modal.Root>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
295
packages/relay/internal/peer.go.txt
Normal file
295
packages/relay/internal/peer.go.txt
Normal file
@@ -0,0 +1,295 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
// "github.com/gorilla/mux"
|
||||
"github.com/hashicorp/memberlist"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// PeerInfo represents information about an SFU peer
|
||||
type PeerInfo struct {
|
||||
NodeID string `json:"nodeId"`
|
||||
Zone string `json:"zone"`
|
||||
PublicIP string `json:"publicIp"`
|
||||
PrivateIP string `json:"privateIp,omitempty"`
|
||||
Streams map[string]bool `json:"streams"` // streamID -> isOrigin
|
||||
}
|
||||
|
||||
// StreamInfo tracks a stream's origin and local subscribers
|
||||
type StreamInfo struct {
|
||||
ID string
|
||||
OriginPeerID string
|
||||
IsLocal bool
|
||||
Publisher *webrtc.PeerConnection
|
||||
Subscribers map[string]*webrtc.PeerConnection
|
||||
InterPeerConn map[string]*webrtc.PeerConnection // connections to other SFU peers
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// DistributedSFU manages streams and peer communication
|
||||
type DistributedSFU struct {
|
||||
nodeID string
|
||||
zone string
|
||||
publicIP string
|
||||
privateIP string
|
||||
streams map[string]*StreamInfo
|
||||
peers map[string]*PeerInfo
|
||||
memberlist *memberlist.Memberlist
|
||||
mu sync.RWMutex
|
||||
config webrtc.Configuration
|
||||
}
|
||||
|
||||
// NewDistributedSFU creates a new distributed SFU instance
|
||||
func NewDistributedSFU(nodeID, zone, publicIP, privateIP string, seeds []string) (*DistributedSFU, error) {
|
||||
sfu := &DistributedSFU{
|
||||
nodeID: nodeID,
|
||||
zone: zone,
|
||||
publicIP: publicIP,
|
||||
privateIP: privateIP,
|
||||
streams: make(map[string]*StreamInfo),
|
||||
peers: make(map[string]*PeerInfo),
|
||||
config: webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{URLs: []string{"stun:stun.l.google.com:19302"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Configure memberlist for peer discovery
|
||||
config := memberlist.DefaultLANConfig()
|
||||
config.Name = nodeID
|
||||
config.BindAddr = privateIP
|
||||
config.AdvertiseAddr = publicIP
|
||||
|
||||
// Add delegate for handling peer updates
|
||||
config.Delegate = &peerDelegate{sfu: sfu}
|
||||
|
||||
// Initialize memberlist
|
||||
list, err := memberlist.Create(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Join the cluster if seeds are provided
|
||||
if len(seeds) > 0 {
|
||||
_, err = list.Join(seeds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
sfu.memberlist = list
|
||||
return sfu, nil
|
||||
}
|
||||
|
||||
// peerDelegate implements memberlist.Delegate
|
||||
type peerDelegate struct {
|
||||
sfu *DistributedSFU
|
||||
}
|
||||
|
||||
// NodeMeta returns metadata about the current node
|
||||
func (d *peerDelegate) NodeMeta(limit int) []byte {
|
||||
meta := PeerInfo{
|
||||
NodeID: d.sfu.nodeID,
|
||||
Zone: d.sfu.zone,
|
||||
PublicIP: d.sfu.publicIP,
|
||||
PrivateIP: d.sfu.privateIP,
|
||||
Streams: make(map[string]bool),
|
||||
}
|
||||
|
||||
d.sfu.mu.RLock()
|
||||
for id, info := range d.sfu.streams {
|
||||
meta.Streams[id] = info.IsLocal
|
||||
}
|
||||
d.sfu.mu.RUnlock()
|
||||
|
||||
data, _ := json.Marshal(meta)
|
||||
return data
|
||||
}
|
||||
|
||||
// NotifyMsg handles peer updates
|
||||
func (d *peerDelegate) NotifyMsg(msg []byte) {
|
||||
var peer PeerInfo
|
||||
if err := json.Unmarshal(msg, &peer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
d.sfu.mu.Lock()
|
||||
d.sfu.peers[peer.NodeID] = &peer
|
||||
|
||||
// Check for new streams we don't have locally
|
||||
for streamID, isOrigin := range peer.Streams {
|
||||
if isOrigin {
|
||||
if _, exists := d.sfu.streams[streamID]; !exists {
|
||||
// Initialize inter-peer connection for this stream
|
||||
d.sfu.initInterPeerStream(streamID, peer.NodeID)
|
||||
}
|
||||
}
|
||||
}
|
||||
d.sfu.mu.Unlock()
|
||||
}
|
||||
|
||||
// initInterPeerStream sets up connection to another SFU for a stream
|
||||
func (sfu *DistributedSFU) initInterPeerStream(streamID, peerID string) {
|
||||
stream := &StreamInfo{
|
||||
ID: streamID,
|
||||
OriginPeerID: peerID,
|
||||
IsLocal: false,
|
||||
Subscribers: make(map[string]*webrtc.PeerConnection),
|
||||
InterPeerConn: make(map[string]*webrtc.PeerConnection),
|
||||
}
|
||||
|
||||
// Create peer connection to the origin SFU
|
||||
pc, err := webrtc.NewPeerConnection(sfu.config)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stream.InterPeerConn[peerID] = pc
|
||||
sfu.streams[streamID] = stream
|
||||
|
||||
// Setup inter-peer WebRTC connection
|
||||
go sfu.establishInterPeerConnection(streamID, peerID, pc)
|
||||
}
|
||||
|
||||
// establishInterPeerConnection handles WebRTC signaling between SFU peers
|
||||
func (sfu *DistributedSFU) establishInterPeerConnection(streamID, peerID string, pc *webrtc.PeerConnection) {
|
||||
// This would typically involve making an HTTP request to the peer's control endpoint
|
||||
// to exchange SDP offers/answers and ICE candidates
|
||||
peerInfo := sfu.peers[peerID]
|
||||
|
||||
// Example endpoint URL construction
|
||||
peerURL := fmt.Sprintf("http://%s:8080/peer/%s/stream/%s",
|
||||
peerInfo.PublicIP, sfu.nodeID, streamID)
|
||||
|
||||
// Handle incoming tracks from peer
|
||||
pc.OnTrack(func(remoteTrack *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
||||
sfu.mu.RLock()
|
||||
stream := sfu.streams[streamID]
|
||||
sfu.mu.RUnlock()
|
||||
|
||||
// Forward the track to local subscribers
|
||||
stream.mu.RLock()
|
||||
for _, subscriber := range stream.Subscribers {
|
||||
localTrack, err := webrtc.NewTrackLocalStaticRTP(
|
||||
remoteTrack.Codec().RTPCodecCapability,
|
||||
remoteTrack.ID(),
|
||||
remoteTrack.StreamID(),
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := subscriber.AddTrack(localTrack); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
packet, _, err := remoteTrack.ReadRTP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := localTrack.WriteRTP(packet); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
stream.mu.RUnlock()
|
||||
})
|
||||
|
||||
// Implement SDP exchange with peer
|
||||
// ... (signaling implementation)
|
||||
}
|
||||
|
||||
// HandleWHIPPublish now includes peer notification
|
||||
func (sfu *DistributedSFU) HandleWHIPPublish(w http.ResponseWriter, r *http.Request) {
|
||||
streamID := mux.Vars(r)["streamID"]
|
||||
|
||||
// Create stream info
|
||||
stream := &StreamInfo{
|
||||
ID: streamID,
|
||||
IsLocal: true,
|
||||
Subscribers: make(map[string]*webrtc.PeerConnection),
|
||||
InterPeerConn: make(map[string]*webrtc.PeerConnection),
|
||||
}
|
||||
|
||||
// ... (rest of WHIP publish logic)
|
||||
|
||||
// Notify other peers about the new stream
|
||||
sfu.broadcastStreamUpdate(streamID, true)
|
||||
}
|
||||
|
||||
// HandleWHEPSubscribe now checks both local and remote streams
|
||||
func (sfu *DistributedSFU) HandleWHEPSubscribe(w http.ResponseWriter, r *http.Request) {
|
||||
streamID := mux.Vars(r)["streamID"]
|
||||
|
||||
sfu.mu.RLock()
|
||||
stream, exists := sfu.streams[streamID]
|
||||
sfu.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
// Check if any peer has this stream
|
||||
if peer := sfu.findStreamPeer(streamID); peer != nil {
|
||||
// Initialize inter-peer connection if needed
|
||||
sfu.initInterPeerStream(streamID, peer.NodeID)
|
||||
} else {
|
||||
http.Error(w, "Stream not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ... (rest of WHEP subscribe logic)
|
||||
}
|
||||
|
||||
// findStreamPeer finds the peer that has the origin of a stream
|
||||
func (sfu *DistributedSFU) findStreamPeer(streamID string) *PeerInfo {
|
||||
sfu.mu.RLock()
|
||||
defer sfu.mu.RUnlock()
|
||||
|
||||
for _, peer := range sfu.peers {
|
||||
if isOrigin, exists := peer.Streams[streamID]; exists && isOrigin {
|
||||
return peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Initialize the distributed SFU
|
||||
sfu, err := NewDistributedSFU(
|
||||
"sfu-1",
|
||||
"us-east",
|
||||
"203.0.113.1",
|
||||
"10.0.0.1",
|
||||
[]string{"203.0.113.2:7946", "203.0.113.3:7946"},
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
router := mux.NewRouter()
|
||||
|
||||
// Regular WHIP/WHEP endpoints
|
||||
router.HandleFunc("/whip/{streamID}", sfu.HandleWHIPPublish).Methods("POST")
|
||||
router.HandleFunc("/whep/{streamID}/{subscriberID}", sfu.HandleWHEPSubscribe).Methods("POST")
|
||||
|
||||
// Inter-peer communication endpoint
|
||||
router.HandleFunc("/peer/{peerID}/stream/{streamID}", sfu.HandlePeerSignaling).Methods("POST")
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":8080",
|
||||
Handler: router,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
server.ListenAndServe()
|
||||
}
|
||||
@@ -18,23 +18,23 @@
|
||||
}
|
||||
|
||||
*::selection {
|
||||
background-color: theme("colors.primary.100");
|
||||
color: theme("colors.primary.500");
|
||||
background-color: theme("colors.gray.400");
|
||||
color: theme("colors.gray.800");
|
||||
}
|
||||
|
||||
*::-moz-selection {
|
||||
background-color: theme("colors.primary.100");
|
||||
color: theme("colors.primary.500");
|
||||
background-color: theme("colors.gray.400");
|
||||
color: theme("colors.gray.800");
|
||||
}
|
||||
|
||||
html.dark *::selection {
|
||||
background-color: theme("colors.primary.800");
|
||||
color: theme("colors.primary.500");
|
||||
background-color: theme("colors.gray.400");
|
||||
color: theme("colors.gray.800");
|
||||
}
|
||||
|
||||
html.dark *::-moz-selection {
|
||||
background-color: theme("colors.primary.800");
|
||||
color: theme("colors.primary.500");
|
||||
background-color: theme("colors.gray.400");
|
||||
color: theme("colors.gray.800");
|
||||
}
|
||||
|
||||
html.dark,
|
||||
@@ -45,13 +45,13 @@
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
*::selection {
|
||||
background-color: theme("colors.primary.900");
|
||||
color: theme("colors.primary.500");
|
||||
background-color: theme("colors.gray.400");
|
||||
color: theme("colors.gray.800");
|
||||
}
|
||||
|
||||
*::-moz-selection {
|
||||
background-color: theme("colors.primary.900");
|
||||
color: theme("colors.primary.500");
|
||||
background-color: theme("colors.gray.400");
|
||||
color: theme("colors.gray.800");
|
||||
}
|
||||
|
||||
html,
|
||||
@@ -242,12 +242,120 @@
|
||||
}
|
||||
}
|
||||
|
||||
.digit_timing{
|
||||
transition: translate 1s linear( 0, 0.0009 8.51%, -0.0047 19.22%, 0.0016 22.39%, 0.023 27.81%,
|
||||
0.0237 30.08%, 0.0144 31.81%, -0.0051 33.48%, -0.1116 39.25%, -0.1181 40.59%,
|
||||
-0.1058 41.79%, -0.0455, 0.0701 45.34%, 0.9702 55.19%, 1.0696 56.97%,
|
||||
1.0987 57.88%, 1.1146 58.82%, 1.1181 59.83%, 1.1092 60.95%, 1.0057 66.48%,
|
||||
0.986 68.14%, 0.9765 69.84%, 0.9769 72.16%, 0.9984 77.61%, 1.0047 80.79%,
|
||||
0.9991 91.48%, 1 );
|
||||
.digit_timing {
|
||||
transition: translate 1s linear(0, 0.0009 8.51%, -0.0047 19.22%, 0.0016 22.39%, 0.023 27.81%,
|
||||
0.0237 30.08%, 0.0144 31.81%, -0.0051 33.48%, -0.1116 39.25%, -0.1181 40.59%,
|
||||
-0.1058 41.79%, -0.0455, 0.0701 45.34%, 0.9702 55.19%, 1.0696 56.97%,
|
||||
1.0987 57.88%, 1.1146 58.82%, 1.1181 59.83%, 1.1092 60.95%, 1.0057 66.48%,
|
||||
0.986 68.14%, 0.9765 69.84%, 0.9769 72.16%, 0.9984 77.61%, 1.0047 80.79%,
|
||||
0.9991 91.48%, 1);
|
||||
translate: 0 calc((var(--v) + 1) * (var(--line-height) * -1));
|
||||
}
|
||||
|
||||
.modal-sheet {
|
||||
animation-duration: 0.5s;
|
||||
animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
|
||||
touch-action: none;
|
||||
will-change: transform;
|
||||
transition: transform 0.5s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
animation-name: slideFromRight;
|
||||
}
|
||||
.modal::backdrop,
|
||||
.modal-sheet::backdrop {
|
||||
animation-duration: 0.5s;
|
||||
animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
|
||||
touch-action: none;
|
||||
will-change: transform;
|
||||
transition: transform 0.5s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
animation-name: fadeIn;
|
||||
}
|
||||
|
||||
.modal-sheet[data-closing] {
|
||||
animation-duration: 0.5s;
|
||||
animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
|
||||
touch-action: none;
|
||||
will-change: transform;
|
||||
transition: transform 0.5s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
animation-name: slideToRight;
|
||||
}
|
||||
|
||||
.modal[data-closing]::backdrop,
|
||||
.modal-sheet[data-closing]::backdrop{
|
||||
animation-duration: 0.5s;
|
||||
animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
|
||||
touch-action: none;
|
||||
will-change: transform;
|
||||
transition: transform 0.5s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
animation-name: fadeOut;
|
||||
}
|
||||
|
||||
.modal {
|
||||
animation-duration: 0.5s;
|
||||
animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
|
||||
touch-action: none;
|
||||
will-change: transform;
|
||||
transition: transform 0.5s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
animation-name: modalIn;
|
||||
}
|
||||
|
||||
.modal[data-closing]{
|
||||
animation-duration: 0.5s;
|
||||
animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
|
||||
touch-action: none;
|
||||
will-change: transform;
|
||||
transition: transform 0.5s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
animation-name: modalOut;
|
||||
}
|
||||
|
||||
@keyframes slideFromRight {
|
||||
from {
|
||||
transform: translate3d(var(--initial-transform, 100%), 0, 0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideToRight {
|
||||
to {
|
||||
transform: translate3d(var(--initial-transform, 100%), 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modalIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
scale: 0.9;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modalOut {
|
||||
to {
|
||||
opacity: 0;
|
||||
scale: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
/* button, a {
|
||||
@apply outline-none hover:[box-shadow:0_0_0_2px_#fcfcfc,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_#fcfcfc,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)]
|
||||
} */
|
||||
@@ -54,6 +54,7 @@
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwind-variants": "^0.2.1",
|
||||
"tailwindcss": "^3.4.9",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.3.3",
|
||||
"valibot": "^0.42.1"
|
||||
}
|
||||
|
||||
@@ -13,4 +13,5 @@ export * as auth from "./popup"
|
||||
export * as Modal from "./modal"
|
||||
export { default as Book } from "./book"
|
||||
export { default as Portal } from "./portal"
|
||||
export { default as Avatar } from "./avatar"
|
||||
export { default as SimpleFooter } from "./simple-footer"
|
||||
@@ -1,4 +1,5 @@
|
||||
import colors from "tailwindcss/colors";
|
||||
import tailwindcssAnimate from "tailwindcss-animate"
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
@@ -154,10 +155,10 @@ export default {
|
||||
"shake": "shake 0.075s 8",
|
||||
"multicolor": "multicolor 5s linear 0s infinite",
|
||||
"zoom-out": "zoom-out 5s ease-out",
|
||||
"fade-in":"fade-in .3s ease forwards"
|
||||
"fade-in": "fade-in .3s ease forwards",
|
||||
},
|
||||
},
|
||||
plugins: []
|
||||
plugins: [tailwindcssAnimate]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user