- {new Array(3).fill(0).map((_, key) => (
-
-
'),url('data:image/svg+xml,
')`
- }}
- >
-
+
+
+
+
+
+ {game.name}
+
+
+
+ {new Array(3).fill(0).map((_, key) => (
+
+
'),url('data:image/svg+xml,
')`
+ }}
+ >
+
+
+
+ ))}
+
+
+
{`${Math.floor(Math.random() * 100)} 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.
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
Teen [13+]
+
Mild Language, Violence, Blood and Gore, Drug References
+
+
+
+
+
+
- ))}
-
-
{`${Math.floor(Math.random() * 100)} open parties you can join`}
-
-
+
+
))}
@@ -180,14 +278,66 @@ export default component$(() => {
Install a game
-
+
{games.map((game, key) => (
-
+
+
+
+
+ {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.
+
+
+
+
+
+
+
+
+
))}
diff --git a/bun.lockb b/bun.lockb
index 03b35cce..f07d1f0a 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/packages/relay/internal/peer.go.txt b/packages/relay/internal/peer.go.txt
new file mode 100644
index 00000000..c42b2032
--- /dev/null
+++ b/packages/relay/internal/peer.go.txt
@@ -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()
+}
diff --git a/packages/ui/globals.css b/packages/ui/globals.css
index 87ec6453..11c0f5b2 100644
--- a/packages/ui/globals.css
+++ b/packages/ui/globals.css
@@ -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)]
+ } */
\ No newline at end of file
diff --git a/packages/ui/package.json b/packages/ui/package.json
index bf7a9848..9ba953d9 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -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"
}
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index f8121d0b..1d930d73 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -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"
\ No newline at end of file
diff --git a/packages/ui/tailwind.config.js b/packages/ui/tailwind.config.js
index 36dc2116..66cba1b6 100644
--- a/packages/ui/tailwind.config.js
+++ b/packages/ui/tailwind.config.js
@@ -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]
}
}