feat(maitred): Update maitred - hookup to the API (#198)

## Description
We are attempting to hookup maitred to the API
Maitred duties will be:
- [ ] Hookup to the API
- [ ]  Wait for signal (from the API) to start Steam
- [ ] Stop signal to stop the gaming session, clean up Steam... and
maybe do the backup

## Summary by CodeRabbit

- **New Features**
- Introduced Docker-based deployment configurations for both the main
and relay applications.
- Added new API endpoints enabling real-time machine messaging and
enhanced IoT operations.
- Expanded database schema and actor types to support improved machine
tracking.

- **Improvements**
- Enhanced real-time communication and relay management with streamlined
room handling.
- Upgraded dependencies, logging, and error handling for greater
stability and performance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
Co-authored-by: Kristian Ollikainen <14197772+DatCaptainHorse@users.noreply.github.com>
This commit is contained in:
Wanjohi
2025-04-07 23:23:53 +03:00
committed by GitHub
parent 6990494b34
commit de80f3e6ab
84 changed files with 7357 additions and 1331 deletions

View File

@@ -0,0 +1,74 @@
package connections
import (
"github.com/pion/webrtc/v4"
"google.golang.org/protobuf/proto"
"log/slog"
gen "relay/internal/proto"
)
// NestriDataChannel is a custom data channel with callbacks
type NestriDataChannel struct {
*webrtc.DataChannel
callbacks map[string]OnMessageCallback // MessageBase type -> callback
}
// NewNestriDataChannel creates a new NestriDataChannel from *webrtc.DataChannel
func NewNestriDataChannel(dc *webrtc.DataChannel) *NestriDataChannel {
ndc := &NestriDataChannel{
DataChannel: dc,
callbacks: make(map[string]OnMessageCallback),
}
// Handler for incoming messages
ndc.OnMessage(func(msg webrtc.DataChannelMessage) {
// If string type message, ignore
if msg.IsString {
return
}
// Decode message
var base gen.ProtoMessageInput
if err := proto.Unmarshal(msg.Data, &base); err != nil {
slog.Error("failed to decode binary DataChannel message", "err", err)
return
}
// Handle message type callback
if callback, ok := ndc.callbacks["input"]; ok {
go callback(msg.Data)
} // TODO: Log unknown message type?
})
return ndc
}
// SendBinary sends a binary message to the data channel
func (ndc *NestriDataChannel) SendBinary(data []byte) error {
return ndc.Send(data)
}
// RegisterMessageCallback registers a callback for a given binary message type
func (ndc *NestriDataChannel) RegisterMessageCallback(msgType string, callback OnMessageCallback) {
if ndc.callbacks == nil {
ndc.callbacks = make(map[string]OnMessageCallback)
}
ndc.callbacks[msgType] = callback
}
// UnregisterMessageCallback removes the callback for a given binary message type
func (ndc *NestriDataChannel) UnregisterMessageCallback(msgType string) {
if ndc.callbacks != nil {
delete(ndc.callbacks, msgType)
}
}
// RegisterOnOpen registers a callback for the data channel opening
func (ndc *NestriDataChannel) RegisterOnOpen(callback func()) {
ndc.OnOpen(callback)
}
// RegisterOnClose registers a callback for the data channel closing
func (ndc *NestriDataChannel) RegisterOnClose(callback func()) {
ndc.OnClose(callback)
}

View File

@@ -0,0 +1,132 @@
package connections
import (
"github.com/pion/webrtc/v4"
"relay/internal/common"
"time"
)
// MessageBase is the base type for WS/DC messages.
type MessageBase struct {
PayloadType string `json:"payload_type"`
Latency *common.LatencyTracker `json:"latency,omitempty"`
}
// MessageLog represents a log message.
type MessageLog struct {
MessageBase
Level string `json:"level"`
Message string `json:"message"`
Time string `json:"time"`
}
// MessageMetrics represents a metrics/heartbeat message.
type MessageMetrics struct {
MessageBase
UsageCPU float64 `json:"usage_cpu"`
UsageMemory float64 `json:"usage_memory"`
Uptime uint64 `json:"uptime"`
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"},
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)
}
// 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,
}
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)
}

View File

@@ -0,0 +1,119 @@
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)
}

View File

@@ -0,0 +1,158 @@
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
}