mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +02:00
⭐ feat: Migrate from WebSocket to libp2p for peer-to-peer connectivity (#286)
## Description Whew, some stuff is still not re-implemented, but it's working! Rabbit's gonna explode with the amount of changes I reckon 😅 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a peer-to-peer relay system using libp2p with enhanced stream forwarding, room state synchronization, and mDNS peer discovery. - Added decentralized room and participant management, metrics publishing, and safe, size-limited, concurrent message streaming with robust framing and callback dispatching. - Implemented asynchronous, callback-driven message handling over custom libp2p streams replacing WebSocket signaling. - **Improvements** - Migrated signaling and stream protocols from WebSocket to libp2p, improving reliability and scalability. - Simplified configuration and environment variables, removing deprecated flags and adding persistent data support. - Enhanced logging, error handling, and connection management for better observability and robustness. - Refined RTP header extension registration and NAT IP handling for improved WebRTC performance. - **Bug Fixes** - Improved ICE candidate buffering and SDP negotiation in WebRTC connections. - Fixed NAT IP and UDP port range configuration issues. - **Refactor** - Modularized codebase, reorganized relay and server logic, and removed deprecated WebSocket-based components. - Streamlined message structures, removed obsolete enums and message types, and simplified SafeMap concurrency. - Replaced WebSocket signaling with libp2p stream protocols in server and relay components. - **Chores** - Updated and cleaned dependencies across Go, Rust, and JavaScript packages. - Added `.gitignore` for persistent data directory in relay package. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com> Co-authored-by: Philipp Neumann <3daquawolf@gmail.com>
This commit is contained in:
committed by
GitHub
parent
e67a8d2b32
commit
6e82eff9e2
@@ -1,12 +1,15 @@
|
||||
package connections
|
||||
|
||||
import (
|
||||
"github.com/pion/webrtc/v4"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"log/slog"
|
||||
gen "relay/internal/proto"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type OnMessageCallback func(data []byte)
|
||||
|
||||
// NestriDataChannel is a custom data channel with callbacks
|
||||
type NestriDataChannel struct {
|
||||
*webrtc.DataChannel
|
||||
@@ -37,7 +40,7 @@ func NewNestriDataChannel(dc *webrtc.DataChannel) *NestriDataChannel {
|
||||
// Handle message type callback
|
||||
if callback, ok := ndc.callbacks["input"]; ok {
|
||||
go callback(msg.Data)
|
||||
} // TODO: Log unknown message type?
|
||||
} // We don't care about unhandled messages
|
||||
})
|
||||
|
||||
return ndc
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
package connections
|
||||
|
||||
import (
|
||||
"github.com/pion/webrtc/v4"
|
||||
"encoding/json"
|
||||
"relay/internal/common"
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// MessageBase is the base type for WS/DC messages.
|
||||
// MessageBase is the base type for any JSON message
|
||||
type MessageBase struct {
|
||||
PayloadType string `json:"payload_type"`
|
||||
Latency *common.LatencyTracker `json:"latency,omitempty"`
|
||||
Type string `json:"payload_type"`
|
||||
Latency *common.LatencyTracker `json:"latency,omitempty"`
|
||||
}
|
||||
|
||||
type MessageRaw struct {
|
||||
MessageBase
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
func NewMessageRaw(t string, data json.RawMessage) *MessageRaw {
|
||||
return &MessageRaw{
|
||||
MessageBase: MessageBase{
|
||||
Type: t,
|
||||
},
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// MessageLog represents a log message.
|
||||
type MessageLog struct {
|
||||
MessageBase
|
||||
Level string `json:"level"`
|
||||
@@ -20,7 +34,17 @@ type MessageLog struct {
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
// MessageMetrics represents a metrics/heartbeat message.
|
||||
func NewMessageLog(t string, level, message, time string) *MessageLog {
|
||||
return &MessageLog{
|
||||
MessageBase: MessageBase{
|
||||
Type: t,
|
||||
},
|
||||
Level: level,
|
||||
Message: message,
|
||||
Time: time,
|
||||
}
|
||||
}
|
||||
|
||||
type MessageMetrics struct {
|
||||
MessageBase
|
||||
UsageCPU float64 `json:"usage_cpu"`
|
||||
@@ -29,104 +53,42 @@ type MessageMetrics struct {
|
||||
PipelineLatency float64 `json:"pipeline_latency"`
|
||||
}
|
||||
|
||||
// MessageICECandidate represents an ICE candidate message.
|
||||
type MessageICECandidate struct {
|
||||
MessageBase
|
||||
Candidate webrtc.ICECandidateInit `json:"candidate"`
|
||||
}
|
||||
|
||||
// MessageSDP represents an SDP message.
|
||||
type MessageSDP struct {
|
||||
MessageBase
|
||||
SDP webrtc.SessionDescription `json:"sdp"`
|
||||
}
|
||||
|
||||
// JoinerType is an enum for the type of incoming room joiner
|
||||
type JoinerType int
|
||||
|
||||
const (
|
||||
JoinerNode JoinerType = iota
|
||||
JoinerClient
|
||||
)
|
||||
|
||||
func (jt *JoinerType) String() string {
|
||||
switch *jt {
|
||||
case JoinerNode:
|
||||
return "node"
|
||||
case JoinerClient:
|
||||
return "client"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// MessageJoin is used to tell us that either participant or ingest wants to join the room
|
||||
type MessageJoin struct {
|
||||
MessageBase
|
||||
JoinerType JoinerType `json:"joiner_type"`
|
||||
}
|
||||
|
||||
// AnswerType is an enum for the type of answer, signaling Room state for a joiner
|
||||
type AnswerType int
|
||||
|
||||
const (
|
||||
AnswerOffline AnswerType = iota // For participant/client, when the room is offline without stream
|
||||
AnswerInUse // For ingest/node joiner, when the room is already in use by another ingest/node
|
||||
AnswerOK // For both, when the join request is handled successfully
|
||||
)
|
||||
|
||||
// MessageAnswer is used to send the answer to a join request
|
||||
type MessageAnswer struct {
|
||||
MessageBase
|
||||
AnswerType AnswerType `json:"answer_type"`
|
||||
}
|
||||
|
||||
// SendLogMessageWS sends a log message to the given WebSocket connection.
|
||||
func (ws *SafeWebSocket) SendLogMessageWS(level, message string) error {
|
||||
msg := MessageLog{
|
||||
MessageBase: MessageBase{PayloadType: "log"},
|
||||
Level: level,
|
||||
Message: message,
|
||||
Time: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
return ws.SendJSON(msg)
|
||||
}
|
||||
|
||||
// SendMetricsMessageWS sends a metrics message to the given WebSocket connection.
|
||||
func (ws *SafeWebSocket) SendMetricsMessageWS(usageCPU, usageMemory float64, uptime uint64, pipelineLatency float64) error {
|
||||
msg := MessageMetrics{
|
||||
MessageBase: MessageBase{PayloadType: "metrics"},
|
||||
func NewMessageMetrics(t string, usageCPU, usageMemory float64, uptime uint64, pipelineLatency float64) *MessageMetrics {
|
||||
return &MessageMetrics{
|
||||
MessageBase: MessageBase{
|
||||
Type: t,
|
||||
},
|
||||
UsageCPU: usageCPU,
|
||||
UsageMemory: usageMemory,
|
||||
Uptime: uptime,
|
||||
PipelineLatency: pipelineLatency,
|
||||
}
|
||||
return ws.SendJSON(msg)
|
||||
}
|
||||
|
||||
// SendICECandidateMessageWS sends an ICE candidate message to the given WebSocket connection.
|
||||
func (ws *SafeWebSocket) SendICECandidateMessageWS(candidate webrtc.ICECandidateInit) error {
|
||||
msg := MessageICECandidate{
|
||||
MessageBase: MessageBase{PayloadType: "ice"},
|
||||
Candidate: candidate,
|
||||
}
|
||||
return ws.SendJSON(msg)
|
||||
type MessageICE struct {
|
||||
MessageBase
|
||||
Candidate webrtc.ICECandidateInit `json:"candidate"`
|
||||
}
|
||||
|
||||
// SendSDPMessageWS sends an SDP message to the given WebSocket connection.
|
||||
func (ws *SafeWebSocket) SendSDPMessageWS(sdp webrtc.SessionDescription) error {
|
||||
msg := MessageSDP{
|
||||
MessageBase: MessageBase{PayloadType: "sdp"},
|
||||
SDP: sdp,
|
||||
func NewMessageICE(t string, candidate webrtc.ICECandidateInit) *MessageICE {
|
||||
return &MessageICE{
|
||||
MessageBase: MessageBase{
|
||||
Type: t,
|
||||
},
|
||||
Candidate: candidate,
|
||||
}
|
||||
return ws.SendJSON(msg)
|
||||
}
|
||||
|
||||
// SendAnswerMessageWS sends an answer message to the given WebSocket connection.
|
||||
func (ws *SafeWebSocket) SendAnswerMessageWS(answer AnswerType) error {
|
||||
msg := MessageAnswer{
|
||||
MessageBase: MessageBase{PayloadType: "answer"},
|
||||
AnswerType: answer,
|
||||
}
|
||||
return ws.SendJSON(msg)
|
||||
type MessageSDP struct {
|
||||
MessageBase
|
||||
SDP webrtc.SessionDescription `json:"sdp"`
|
||||
}
|
||||
|
||||
func NewMessageSDP(t string, sdp webrtc.SessionDescription) *MessageSDP {
|
||||
return &MessageSDP{
|
||||
MessageBase: MessageBase{
|
||||
Type: t,
|
||||
},
|
||||
SDP: sdp,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
package connections
|
||||
|
||||
import (
|
||||
"github.com/pion/webrtc/v4"
|
||||
"google.golang.org/protobuf/proto"
|
||||
gen "relay/internal/proto"
|
||||
)
|
||||
|
||||
// SendMeshHandshake sends a handshake message to another relay.
|
||||
func (ws *SafeWebSocket) SendMeshHandshake(relayID, publicKey string) error {
|
||||
msg := &gen.MeshMessage{
|
||||
Type: &gen.MeshMessage_Handshake{
|
||||
Handshake: &gen.Handshake{
|
||||
RelayId: relayID,
|
||||
DhPublicKey: publicKey,
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ws.SendBinary(data)
|
||||
}
|
||||
|
||||
// SendMeshHandshakeResponse sends a handshake response to a relay.
|
||||
func (ws *SafeWebSocket) SendMeshHandshakeResponse(relayID, dhPublicKey string, approvals map[string]string) error {
|
||||
msg := &gen.MeshMessage{
|
||||
Type: &gen.MeshMessage_HandshakeResponse{
|
||||
HandshakeResponse: &gen.HandshakeResponse{
|
||||
RelayId: relayID,
|
||||
DhPublicKey: dhPublicKey,
|
||||
Approvals: approvals,
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ws.SendBinary(data)
|
||||
}
|
||||
|
||||
// SendMeshForwardSDP sends a forwarded SDP message to another relay
|
||||
func (ws *SafeWebSocket) SendMeshForwardSDP(roomName, participantID string, sdp webrtc.SessionDescription) error {
|
||||
msg := &gen.MeshMessage{
|
||||
Type: &gen.MeshMessage_ForwardSdp{
|
||||
ForwardSdp: &gen.ForwardSDP{
|
||||
RoomName: roomName,
|
||||
ParticipantId: participantID,
|
||||
Sdp: sdp.SDP,
|
||||
Type: sdp.Type.String(),
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ws.SendBinary(data)
|
||||
}
|
||||
|
||||
// SendMeshForwardICE sends a forwarded ICE candidate to another relay
|
||||
func (ws *SafeWebSocket) SendMeshForwardICE(roomName, participantID string, candidate webrtc.ICECandidateInit) error {
|
||||
var sdpMLineIndex uint32
|
||||
if candidate.SDPMLineIndex != nil {
|
||||
sdpMLineIndex = uint32(*candidate.SDPMLineIndex)
|
||||
}
|
||||
|
||||
msg := &gen.MeshMessage{
|
||||
Type: &gen.MeshMessage_ForwardIce{
|
||||
ForwardIce: &gen.ForwardICE{
|
||||
RoomName: roomName,
|
||||
ParticipantId: participantID,
|
||||
Candidate: &gen.ICECandidateInit{
|
||||
Candidate: candidate.Candidate,
|
||||
SdpMid: candidate.SDPMid,
|
||||
SdpMLineIndex: &sdpMLineIndex,
|
||||
UsernameFragment: candidate.UsernameFragment,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ws.SendBinary(data)
|
||||
}
|
||||
|
||||
func (ws *SafeWebSocket) SendMeshForwardIngest(roomName string) error {
|
||||
msg := &gen.MeshMessage{
|
||||
Type: &gen.MeshMessage_ForwardIngest{
|
||||
ForwardIngest: &gen.ForwardIngest{
|
||||
RoomName: roomName,
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ws.SendBinary(data)
|
||||
}
|
||||
|
||||
func (ws *SafeWebSocket) SendMeshStreamRequest(roomName string) error {
|
||||
msg := &gen.MeshMessage{
|
||||
Type: &gen.MeshMessage_StreamRequest{
|
||||
StreamRequest: &gen.StreamRequest{
|
||||
RoomName: roomName,
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ws.SendBinary(data)
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package connections
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gorilla/websocket"
|
||||
"log/slog"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// OnMessageCallback is a callback for messages of given type
|
||||
type OnMessageCallback func(data []byte)
|
||||
|
||||
// SafeWebSocket is a websocket with a mutex
|
||||
type SafeWebSocket struct {
|
||||
*websocket.Conn
|
||||
sync.Mutex
|
||||
closed bool
|
||||
closeCallback func() // Callback to call on close
|
||||
closeChan chan struct{} // Channel to signal closure
|
||||
callbacks map[string]OnMessageCallback // MessageBase type -> callback
|
||||
binaryCallback OnMessageCallback // Binary message callback
|
||||
sharedSecret []byte
|
||||
}
|
||||
|
||||
// NewSafeWebSocket creates a new SafeWebSocket from *websocket.Conn
|
||||
func NewSafeWebSocket(conn *websocket.Conn) *SafeWebSocket {
|
||||
ws := &SafeWebSocket{
|
||||
Conn: conn,
|
||||
closed: false,
|
||||
closeCallback: nil,
|
||||
closeChan: make(chan struct{}),
|
||||
callbacks: make(map[string]OnMessageCallback),
|
||||
binaryCallback: nil,
|
||||
sharedSecret: nil,
|
||||
}
|
||||
|
||||
// Launch a goroutine to handle messages
|
||||
go func() {
|
||||
for {
|
||||
// Read message
|
||||
kind, data, err := ws.Conn.ReadMessage()
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure, websocket.CloseNoStatusReceived) {
|
||||
// If unexpected close error, break
|
||||
slog.Debug("WebSocket closed unexpectedly", "err", err)
|
||||
break
|
||||
} else if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseAbnormalClosure, websocket.CloseNoStatusReceived) {
|
||||
break
|
||||
} else if err != nil {
|
||||
slog.Error("Failed reading WebSocket message", "err", err)
|
||||
break
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case websocket.TextMessage:
|
||||
// Decode message
|
||||
var msg MessageBase
|
||||
if err = json.Unmarshal(data, &msg); err != nil {
|
||||
slog.Error("Failed decoding WebSocket message", "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle message type callback
|
||||
if callback, ok := ws.callbacks[msg.PayloadType]; ok {
|
||||
callback(data)
|
||||
} // TODO: Log unknown message payload type?
|
||||
break
|
||||
case websocket.BinaryMessage:
|
||||
// Handle binary message callback
|
||||
if ws.binaryCallback != nil {
|
||||
ws.binaryCallback(data)
|
||||
}
|
||||
break
|
||||
default:
|
||||
slog.Warn("Unknown WebSocket message type", "type", kind)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Signal closure to callback first
|
||||
if ws.closeCallback != nil {
|
||||
ws.closeCallback()
|
||||
}
|
||||
close(ws.closeChan)
|
||||
ws.closed = true
|
||||
}()
|
||||
|
||||
return ws
|
||||
}
|
||||
|
||||
// SetSharedSecret sets the shared secret for the websocket
|
||||
func (ws *SafeWebSocket) SetSharedSecret(secret []byte) {
|
||||
ws.sharedSecret = secret
|
||||
}
|
||||
|
||||
// GetSharedSecret returns the shared secret for the websocket
|
||||
func (ws *SafeWebSocket) GetSharedSecret() []byte {
|
||||
return ws.sharedSecret
|
||||
}
|
||||
|
||||
// SendJSON writes JSON to a websocket with a mutex
|
||||
func (ws *SafeWebSocket) SendJSON(v interface{}) error {
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
return ws.Conn.WriteJSON(v)
|
||||
}
|
||||
|
||||
// SendBinary writes binary to a websocket with a mutex
|
||||
func (ws *SafeWebSocket) SendBinary(data []byte) error {
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
return ws.Conn.WriteMessage(websocket.BinaryMessage, data)
|
||||
}
|
||||
|
||||
// RegisterMessageCallback sets the callback for binary message of given type
|
||||
func (ws *SafeWebSocket) RegisterMessageCallback(msgType string, callback OnMessageCallback) {
|
||||
if ws.callbacks == nil {
|
||||
ws.callbacks = make(map[string]OnMessageCallback)
|
||||
}
|
||||
ws.callbacks[msgType] = callback
|
||||
}
|
||||
|
||||
// RegisterBinaryMessageCallback sets the callback for all binary messages
|
||||
func (ws *SafeWebSocket) RegisterBinaryMessageCallback(callback OnMessageCallback) {
|
||||
ws.binaryCallback = callback
|
||||
}
|
||||
|
||||
// UnregisterMessageCallback removes the callback for binary message of given type
|
||||
func (ws *SafeWebSocket) UnregisterMessageCallback(msgType string) {
|
||||
if ws.callbacks != nil {
|
||||
delete(ws.callbacks, msgType)
|
||||
}
|
||||
}
|
||||
|
||||
// UnregisterBinaryMessageCallback removes the callback for all binary messages
|
||||
func (ws *SafeWebSocket) UnregisterBinaryMessageCallback() {
|
||||
ws.binaryCallback = nil
|
||||
}
|
||||
|
||||
// RegisterOnClose sets the callback for websocket closing
|
||||
func (ws *SafeWebSocket) RegisterOnClose(callback func()) {
|
||||
ws.closeCallback = func() {
|
||||
// Clear our callbacks
|
||||
ws.callbacks = nil
|
||||
ws.binaryCallback = nil
|
||||
// Call the callback
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
// Closed returns a channel that closes when the WebSocket connection is terminated
|
||||
func (ws *SafeWebSocket) Closed() <-chan struct{} {
|
||||
return ws.closeChan
|
||||
}
|
||||
|
||||
// IsClosed returns true if the WebSocket connection is closed
|
||||
func (ws *SafeWebSocket) IsClosed() bool {
|
||||
return ws.closed
|
||||
}
|
||||
Reference in New Issue
Block a user