mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 16:55:37 +02:00
⭐ feat(www): Finish up on the UI components (#158)
This commit is contained in:
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