Files
netris-nestri/packages/relay/internal/shared/participant.go
Kristian Ollikainen d87a0b35dd feat: Fully use protobuf, fix controller issues and cleanup (#305)
## Description
### First commit
Restructured protobuf schemas to make them easier to use across
languages, switched to using them in-place of JSON for signaling as
well, so there's no 2 different message formats flying about. Few new
message types to deal with clients and nestri-servers better (not final
format, may see changes still).

General cleanup of dead/unused code along some bug squashing and package
updates.

TODO for future commits:
- [x] Fix additional controllers not doing inputs (possibly needs
vimputti changes)
- [x] ~~Restructure relay protocols code a bit, to reduce bloatiness of
the currently single file for them, more code re-use.~~
- Gonna keep this PR somewhat manageable without poking more at relay..
- [x] ~~Try to fix issue where with multiple clients, static stream
content causes video to freeze until there's some movement.~~
- Was caused by server tuned profile being `throughput-performance`,
causing CPU latency to be too high.
- [x] Ponder the orb


### Second + third commit
Redid the controller polling handling and fixed multi-controller
handling in vimputti and nestri code sides. Remove some dead relay code
as well to clean up the protocol source file, we'll revisit the meshing
functionality later.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added software rendering option and MangoHud runtime config;
controller sessions now support reconnection and batched state updates
with persistent session IDs.

* **Bug Fixes**
* Restored previously-filtered NES-like gamepads so they connect
correctly.

* **Chores**
* Modernized dependencies and protobuf tooling, migrated to
protobuf-based messaging and streaming, and removed obsolete CUDA build
steps.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
2025-11-08 13:10:28 +02:00

140 lines
3.6 KiB
Go

package shared
import (
"errors"
"fmt"
"io"
"log/slog"
"relay/internal/common"
"relay/internal/connections"
"sync"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/oklog/ulid/v2"
"github.com/pion/webrtc/v4"
)
type Participant struct {
ID ulid.ULID
SessionID string // Track session for reconnection
PeerID peer.ID // libp2p peer ID
PeerConnection *webrtc.PeerConnection
DataChannel *connections.NestriDataChannel
// Per-viewer tracks and channels
VideoTrack *webrtc.TrackLocalStaticRTP
AudioTrack *webrtc.TrackLocalStaticRTP
// Per-viewer RTP state for retiming
VideoSequenceNumber uint16
VideoTimestamp uint32
AudioSequenceNumber uint16
AudioTimestamp uint32
packetQueue chan *participantPacket
closeOnce sync.Once
}
func NewParticipant(sessionID string, peerID peer.ID) (*Participant, error) {
id, err := common.NewULID()
if err != nil {
return nil, fmt.Errorf("failed to create ULID for Participant: %w", err)
}
p := &Participant{
ID: id,
SessionID: sessionID,
PeerID: peerID,
VideoSequenceNumber: 0,
VideoTimestamp: 0,
AudioSequenceNumber: 0,
AudioTimestamp: 0,
packetQueue: make(chan *participantPacket, 1000),
}
go p.packetWriter()
return p, nil
}
// SetTrack sets audio/video track for Participant
func (p *Participant) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalStaticRTP) {
switch trackType {
case webrtc.RTPCodecTypeAudio:
p.AudioTrack = track
_, err := p.PeerConnection.AddTrack(track)
if err != nil {
slog.Error("Failed to add audio track", "participant", p.ID, "err", err)
}
case webrtc.RTPCodecTypeVideo:
p.VideoTrack = track
_, err := p.PeerConnection.AddTrack(track)
if err != nil {
slog.Error("Failed to add video track", "participant", p.ID, "err", err)
}
default:
slog.Warn("Unknown track type", "participant", p.ID, "trackType", trackType)
}
}
// Close cleans up participant resources
func (p *Participant) Close() {
p.closeOnce.Do(func() {
close(p.packetQueue)
})
if p.DataChannel != nil {
err := p.DataChannel.Close()
if err != nil {
slog.Error("Failed to close DataChannel", "participant", p.ID, "err", err)
}
p.DataChannel = nil
}
if p.PeerConnection != nil {
err := p.PeerConnection.Close()
if err != nil {
slog.Error("Failed to close PeerConnection", "participant", p.ID, "err", err)
}
p.PeerConnection = nil
}
if p.VideoTrack != nil {
p.VideoTrack = nil
}
if p.AudioTrack != nil {
p.AudioTrack = nil
}
}
func (p *Participant) packetWriter() {
for pkt := range p.packetQueue {
var track *webrtc.TrackLocalStaticRTP
var sequenceNumber uint16
var timestamp uint32
// No mutex needed - only this goroutine modifies these
if pkt.kind == webrtc.RTPCodecTypeAudio {
track = p.AudioTrack
p.AudioSequenceNumber = uint16(int(p.AudioSequenceNumber) + pkt.sequenceDiff)
p.AudioTimestamp = uint32(int64(p.AudioTimestamp) + pkt.timeDiff)
sequenceNumber = p.AudioSequenceNumber
timestamp = p.AudioTimestamp
} else {
track = p.VideoTrack
p.VideoSequenceNumber = uint16(int(p.VideoSequenceNumber) + pkt.sequenceDiff)
p.VideoTimestamp = uint32(int64(p.VideoTimestamp) + pkt.timeDiff)
sequenceNumber = p.VideoSequenceNumber
timestamp = p.VideoTimestamp
}
if track != nil {
pkt.packet.SequenceNumber = sequenceNumber
pkt.packet.Timestamp = timestamp
if err := track.WriteRTP(pkt.packet); err != nil && !errors.Is(err, io.ErrClosedPipe) {
slog.Error("WriteRTP failed", "participant", p.ID, "kind", pkt.kind, "err", err)
}
}
// Return packet struct to pool
participantPacketPool.Put(pkt)
}
}