node build fixed

This commit is contained in:
ra_ma
2025-09-20 14:08:38 +01:00
parent c6ebbe069d
commit 3d298fa434
1516 changed files with 535727 additions and 2 deletions

View File

@@ -0,0 +1,421 @@
package nakama
import (
"context"
"encoding/json"
debrid_client "seanime/internal/debrid/client"
"seanime/internal/library/playbackmanager"
"seanime/internal/mediaplayers/mediaplayer"
"seanime/internal/torrentstream"
"sync"
"time"
"github.com/rs/zerolog"
"github.com/samber/mo"
)
const (
// Host -> Peer
MessageTypeWatchPartyCreated = "watch_party_created" // Host creates a watch party
MessageTypeWatchPartyStateChanged = "watch_party_state_changed" // Host or peer changes the state of the watch party
MessageTypeWatchPartyStopped = "watch_party_stopped" // Host stops a watch party
MessageTypeWatchPartyPlaybackStatus = "watch_party_playback_status" // Host or peer sends playback status to peers (seek, play, pause, etc)
MessageTypeWatchPartyPlaybackStopped = "watch_party_playback_stopped" // Peer sends playback stopped to host
// MessageTypeWatchPartyRelayModeStreamReady = "watch_party_relay_mode_stream_ready" // Relay server signals to origin that the stream is ready
MessageTypeWatchPartyRelayModePeersReady = "watch_party_relay_mode_peers_ready" // Relay server signals to origin that all peers are ready
MessageTypeWatchPartyRelayModePeerBuffering = "watch_party_relay_mode_peer_buffering" // Relay server signals to origin the buffering status (tells origin to pause/unpause)
// Peer -> Host
MessageTypeWatchPartyJoin = "watch_party_join" // Peer joins a watch party
MessageTypeWatchPartyLeave = "watch_party_leave" // Peer leaves a watch party
MessageTypeWatchPartyPeerStatus = "watch_party_peer_status" // Peer reports their current status to host
MessageTypeWatchPartyBufferUpdate = "watch_party_buffer_update" // Peer reports buffering state to host
MessageTypeWatchPartyRelayModeOriginStreamStarted = "watch_party_relay_mode_origin_stream_started" // Relay origin sends is starting a stream, the host will start it too
MessageTypeWatchPartyRelayModeOriginPlaybackStatus = "watch_party_relay_mode_origin_playback_status" // Relay origin sends playback status to relay server
MessageTypeWatchPartyRelayModeOriginPlaybackStopped = "watch_party_relay_mode_origin_playback_stopped" // Relay origin sends playback stopped to relay server
)
const (
// Drift detection and sync thresholds
MinSyncThreshold = 0.8 // Minimum sync threshold to prevent excessive seeking
MaxSyncThreshold = 5.0 // Maximum sync threshold for loose synchronization
AggressiveSyncMultiplier = 0.4 // Multiplier for large drift (>3s) to sync aggressively
ModerateSyncMultiplier = 0.6 // Multiplier for medium drift (>1.5s) to sync more frequently
// Sync timing and delays
MinSeekDelay = 200 * time.Millisecond // Minimum delay for seek operations
MaxSeekDelay = 600 * time.Millisecond // Maximum delay for seek operations
DefaultSeekCooldown = 1 * time.Second // Cooldown between consecutive seeks
// Message staleness and processing
MaxMessageAge = 1.5 // Seconds to ignore stale sync messages
PendingSeekWaitMultiplier = 1.0 // Multiplier for pending seek wait time
// Position and state detection
SignificantPositionJump = 3.0 // Seconds to detect seeking vs normal playback
ResumePositionDriftThreshold = 1.0 // Seconds of drift before syncing on resume
ResumeAheadTolerance = 2.0 // Seconds ahead tolerance to prevent jitter on resume
PausePositionSyncThreshold = 0.7 // Seconds of drift threshold for pause sync
// Catch-up and buffering
CatchUpBehindThreshold = 2.0 // Seconds behind before starting catch-up
CatchUpToleranceThreshold = 0.5 // Seconds within target to stop catch-up
MaxCatchUpDuration = 4 * time.Second // Maximum duration for catch-up operations
CatchUpTickInterval = 200 * time.Millisecond // Interval for catch-up progress checks
// Buffer detection (peer-side)
BufferDetectionMinInterval = 1.5 // Seconds between buffer health checks
BufferDetectionTolerance = 0.6 // Tolerance for playback progress detection
BufferDetectionStallThreshold = 2 // Consecutive stalls before buffering detection
BufferHealthDecrement = 0.15 // Buffer health decrease per stall
EndOfContentThreshold = 2.0 // Seconds from end to disable buffering detection
// Network and timing compensation
MinDynamicDelay = 200 * time.Millisecond // Minimum network delay compensation
MaxDynamicDelay = 500 * time.Millisecond // Maximum network delay compensation
)
type WatchPartyManager struct {
logger *zerolog.Logger
manager *Manager
currentSession mo.Option[*WatchPartySession] // Current watch party session
sessionCtx context.Context // Context for the current watch party session
sessionCtxCancel context.CancelFunc // Cancel function for the current watch party session
mu sync.RWMutex // Mutex for the watch party manager
// Seek management to prevent choppy playback
lastSeekTime time.Time // Time of last seek operation
seekCooldown time.Duration // Minimum time between seeks
// Catch-up management
catchUpCancel context.CancelFunc // Cancel function for catch-up operations
catchUpMu sync.Mutex // Mutex for catch-up operations
// Seek management
pendingSeekTime time.Time // When a seek was initiated
pendingSeekPosition float64 // Position we're seeking to
seekMu sync.Mutex // Mutex for seek state
// Buffering management (host only)
bufferWaitStart time.Time // When we started waiting for peers to buffer
isWaitingForBuffers bool // Whether we're currently waiting for peers to be ready
bufferMu sync.Mutex // Mutex for buffer state changes
statusReportTicker *time.Ticker // Ticker for peer status reporting
statusReportCancel context.CancelFunc // Cancel function for status reporting
waitForPeersCancel context.CancelFunc // Cancel function for waitForPeersReady goroutine
// Buffering detection (peer only)
bufferDetectionMu sync.Mutex // Mutex for buffering detection state
lastPosition float64 // Last known playback position
lastPositionTime time.Time // When we last updated the position
stallCount int // Number of consecutive stalls detected
lastPlayState bool // Last known play/pause state to detect rapid changes
lastPlayStateTime time.Time // When we last changed play state
// Sequence-based message ordering
sequenceMu sync.Mutex // Mutex for sequence number operations
sendSequence uint64 // Current sequence number for outgoing messages
lastRxSequence uint64 // Latest received sequence number
// Peer
peerPlaybackListener *playbackmanager.PlaybackStatusSubscriber // Listener for playback status changes (can be nil)
}
type WatchPartySession struct {
ID string `json:"id"`
Participants map[string]*WatchPartySessionParticipant `json:"participants"`
Settings *WatchPartySessionSettings `json:"settings"`
CreatedAt time.Time `json:"createdAt"`
CurrentMediaInfo *WatchPartySessionMediaInfo `json:"currentMediaInfo"` // can be nil if not set
IsRelayMode bool `json:"isRelayMode"` // Whether this session is in relay mode
mu sync.RWMutex `json:"-"`
}
type WatchPartySessionParticipant struct {
ID string `json:"id"` // PeerID (UUID) for unique identification
Username string `json:"username"` // Display name
IsHost bool `json:"isHost"`
CanControl bool `json:"canControl"`
IsReady bool `json:"isReady"`
LastSeen time.Time `json:"lastSeen"`
Latency int64 `json:"latency"` // in milliseconds
// Buffering state
IsBuffering bool `json:"isBuffering"`
BufferHealth float64 `json:"bufferHealth"` // 0.0 to 1.0, how much buffer is available
PlaybackStatus *mediaplayer.PlaybackStatus `json:"playbackStatus,omitempty"` // Current playback status
// Relay mode
IsRelayOrigin bool `json:"isRelayOrigin"` // Whether this peer is the origin for relay mode
}
type WatchPartySessionMediaInfo struct {
MediaId int `json:"mediaId"`
EpisodeNumber int `json:"episodeNumber"`
AniDBEpisode string `json:"aniDbEpisode"`
StreamType string `json:"streamType"` // "file", "torrent", "debrid", "online"
StreamPath string `json:"streamPath"` // URL for stream playback (e.g. /api/v1/nakama/stream?type=file&path=...)
OnlineStreamParams *OnlineStreamParams `json:"onlineStreamParams,omitempty"`
OptionalTorrentStreamStartOptions *torrentstream.StartStreamOptions `json:"optionalTorrentStreamStartOptions,omitempty"`
}
type OnlineStreamParams struct {
MediaId int `json:"mediaId"`
Provider string `json:"provider"`
Server string `json:"server"`
Dubbed bool `json:"dubbed"`
EpisodeNumber int `json:"episodeNumber"`
Quality string `json:"quality"`
}
type WatchPartySessionSettings struct {
SyncThreshold float64 `json:"syncThreshold"` // Seconds of desync before forcing sync
MaxBufferWaitTime int `json:"maxBufferWaitTime"` // Max time to wait for buffering peers (seconds)
}
// Events
type (
WatchPartyCreatedPayload struct {
Session *WatchPartySession `json:"session"`
}
WatchPartyJoinPayload struct {
PeerId string `json:"peerId"`
Username string `json:"username"`
}
WatchPartyLeavePayload struct {
PeerId string `json:"peerId"`
}
WatchPartyPlaybackStatusPayload struct {
PlaybackStatus mediaplayer.PlaybackStatus `json:"playbackStatus"`
Timestamp int64 `json:"timestamp"` // Unix nano timestamp
SequenceNumber uint64 `json:"sequenceNumber"`
EpisodeNumber int `json:"episodeNumber"` // For episode changes
}
WatchPartyStateChangedPayload struct {
Session *WatchPartySession `json:"session"`
}
WatchPartyPeerStatusPayload struct {
PeerId string `json:"peerId"`
PlaybackStatus mediaplayer.PlaybackStatus `json:"playbackStatus"`
IsBuffering bool `json:"isBuffering"`
BufferHealth float64 `json:"bufferHealth"` // 0.0 to 1.0
Timestamp time.Time `json:"timestamp"`
}
WatchPartyBufferUpdatePayload struct {
PeerId string `json:"peerId"`
IsBuffering bool `json:"isBuffering"`
BufferHealth float64 `json:"bufferHealth"`
Timestamp time.Time `json:"timestamp"`
}
WatchPartyEnableRelayModePayload struct {
PeerId string `json:"peerId"` // PeerID of the peer to promote to origin
}
WatchPartyRelayModeOriginStreamStartedPayload struct {
Filename string `json:"filename"`
Filepath string `json:"filepath"`
StreamType string `json:"streamType"`
OptionalLocalPath string `json:"optionalLocalPath,omitempty"`
OptionalTorrentStreamStartOptions *torrentstream.StartStreamOptions `json:"optionalTorrentStreamStartOptions,omitempty"`
OptionalDebridStreamStartOptions *debrid_client.StartStreamOptions `json:"optionalDebridStreamStartOptions,omitempty"`
Status mediaplayer.PlaybackStatus `json:"status"`
State playbackmanager.PlaybackState `json:"state"`
}
WatchPartyRelayModeOriginPlaybackStatusPayload struct {
Status mediaplayer.PlaybackStatus `json:"status"`
State playbackmanager.PlaybackState `json:"state"`
Timestamp int64 `json:"timestamp"`
}
)
func NewWatchPartyManager(manager *Manager) *WatchPartyManager {
return &WatchPartyManager{
logger: manager.logger,
manager: manager,
seekCooldown: DefaultSeekCooldown,
}
}
// Cleanup stops all goroutines and cleans up resources to prevent memory leaks
func (wpm *WatchPartyManager) Cleanup() {
wpm.mu.Lock()
defer wpm.mu.Unlock()
if wpm.currentSession.IsPresent() {
go wpm.LeaveWatchParty()
go wpm.StopWatchParty()
}
wpm.logger.Debug().Msg("nakama: Cleaning up watch party manager")
// Stop status reporting (peer side)
wpm.stopStatusReporting()
// Cancel any ongoing catch-up operations
wpm.cancelCatchUp()
// Clean up seek management state
wpm.seekMu.Lock()
wpm.pendingSeekTime = time.Time{}
wpm.pendingSeekPosition = 0
wpm.seekMu.Unlock()
// Cancel waitForPeersReady goroutine (host side)
wpm.bufferMu.Lock()
if wpm.waitForPeersCancel != nil {
wpm.waitForPeersCancel()
wpm.waitForPeersCancel = nil
}
wpm.isWaitingForBuffers = false
wpm.bufferMu.Unlock()
// Cancel session context (stops all session-related goroutines)
if wpm.sessionCtxCancel != nil {
wpm.sessionCtxCancel()
wpm.sessionCtx = nil
wpm.sessionCtxCancel = nil
}
// Clear session
wpm.currentSession = mo.None[*WatchPartySession]()
wpm.logger.Debug().Msg("nakama: Watch party manager cleanup completed")
}
// GetCurrentSession returns the current watch party session if it exists
func (wpm *WatchPartyManager) GetCurrentSession() (*WatchPartySession, bool) {
wpm.mu.RLock()
defer wpm.mu.RUnlock()
session, ok := wpm.currentSession.Get()
return session, ok
}
func (wpm *WatchPartyManager) handleMessage(message *Message, senderID string) error {
marshaledPayload, err := json.Marshal(message.Payload)
if err != nil {
return err
}
// wpm.logger.Debug().Str("type", string(message.Type)).Interface("payload", message.Payload).Msg("nakama: Received watch party message")
switch message.Type {
case MessageTypeWatchPartyStateChanged:
// wpm.logger.Debug().Msg("nakama: Received watch party state changed message")
var payload WatchPartyStateChangedPayload
err := json.Unmarshal(marshaledPayload, &payload)
if err != nil {
return err
}
wpm.handleWatchPartyStateChangedEvent(&payload)
case MessageTypeWatchPartyCreated:
wpm.logger.Debug().Msg("nakama: Received watch party created message")
var payload WatchPartyCreatedPayload
err := json.Unmarshal(marshaledPayload, &payload)
if err != nil {
return err
}
wpm.handleWatchPartyCreatedEvent(&payload)
case MessageTypeWatchPartyStopped:
wpm.logger.Debug().Msg("nakama: Received watch party stopped message")
wpm.handleWatchPartyStoppedEvent()
case MessageTypeWatchPartyJoin:
wpm.logger.Debug().Msg("nakama: Received watch party join message")
var payload WatchPartyJoinPayload
err := json.Unmarshal(marshaledPayload, &payload)
if err != nil {
return err
}
wpm.handleWatchPartyPeerJoinedEvent(&payload, message.Timestamp)
case MessageTypeWatchPartyLeave:
wpm.logger.Debug().Msg("nakama: Received watch party leave message")
var payload WatchPartyLeavePayload
err := json.Unmarshal(marshaledPayload, &payload)
if err != nil {
return err
}
wpm.handleWatchPartyPeerLeftEvent(&payload)
case MessageTypeWatchPartyPeerStatus:
//wpm.logger.Debug().Msg("nakama: Received watch party peer status message")
var payload WatchPartyPeerStatusPayload
err := json.Unmarshal(marshaledPayload, &payload)
if err != nil {
return err
}
wpm.handleWatchPartyPeerStatusEvent(&payload)
case MessageTypeWatchPartyBufferUpdate:
//wpm.logger.Debug().Msg("nakama: Received watch party buffer update message")
var payload WatchPartyBufferUpdatePayload
err := json.Unmarshal(marshaledPayload, &payload)
if err != nil {
return err
}
wpm.handleWatchPartyBufferUpdateEvent(&payload)
case MessageTypeWatchPartyPlaybackStatus:
// wpm.logger.Debug().Msg("nakama: Received watch party playback status message")
var payload WatchPartyPlaybackStatusPayload
err := json.Unmarshal(marshaledPayload, &payload)
if err != nil {
return err
}
wpm.handleWatchPartyPlaybackStatusEvent(&payload)
case MessageTypeWatchPartyRelayModeOriginStreamStarted:
wpm.logger.Debug().Msg("nakama: Received relay mode stream from origin message")
var payload WatchPartyRelayModeOriginStreamStartedPayload
err := json.Unmarshal(marshaledPayload, &payload)
if err != nil {
return err
}
wpm.handleWatchPartyRelayModeOriginStreamStartedEvent(&payload)
case MessageTypeWatchPartyRelayModePeerBuffering:
// TODO: Implement
case MessageTypeWatchPartyRelayModePeersReady:
wpm.logger.Debug().Msg("nakama: Received relay mode peers ready message")
wpm.handleWatchPartyRelayModePeersReadyEvent()
case MessageTypeWatchPartyRelayModeOriginPlaybackStatus:
// wpm.logger.Debug().Msg("nakama: Received relay mode origin playback status message")
var payload WatchPartyRelayModeOriginPlaybackStatusPayload
err := json.Unmarshal(marshaledPayload, &payload)
if err != nil {
return err
}
wpm.handleWatchPartyRelayModeOriginPlaybackStatusEvent(&payload)
case MessageTypeWatchPartyRelayModeOriginPlaybackStopped:
wpm.logger.Debug().Msg("nakama: Received relay mode origin playback stopped message")
wpm.handleWatchPartyRelayModeOriginPlaybackStoppedEvent()
}
return nil
}
func (mi *WatchPartySessionMediaInfo) Equals(other *WatchPartySessionMediaInfo) bool {
if mi == nil || other == nil {
return false
}
return mi.MediaId == other.MediaId &&
mi.EpisodeNumber == other.EpisodeNumber &&
mi.AniDBEpisode == other.AniDBEpisode &&
mi.StreamType == other.StreamType &&
mi.StreamPath == other.StreamPath
}