859 lines
25 KiB
Go
859 lines
25 KiB
Go
package nakama
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
debrid_client "seanime/internal/debrid/client"
|
|
"seanime/internal/events"
|
|
"seanime/internal/library/playbackmanager"
|
|
"seanime/internal/mediaplayers/mediaplayer"
|
|
"seanime/internal/torrentstream"
|
|
"seanime/internal/util"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/samber/mo"
|
|
)
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Host
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type CreateWatchOptions struct {
|
|
Settings *WatchPartySessionSettings `json:"settings"`
|
|
}
|
|
|
|
// CreateWatchParty creates a new watch party (host only)
|
|
func (wpm *WatchPartyManager) CreateWatchParty(options *CreateWatchOptions) (*WatchPartySession, error) {
|
|
wpm.mu.Lock()
|
|
defer wpm.mu.Unlock()
|
|
|
|
if !wpm.manager.IsHost() {
|
|
return nil, errors.New("only hosts can create watch parties")
|
|
}
|
|
|
|
if wpm.sessionCtxCancel != nil {
|
|
wpm.sessionCtxCancel()
|
|
wpm.sessionCtx = nil
|
|
wpm.sessionCtxCancel = nil
|
|
wpm.currentSession = mo.None[*WatchPartySession]()
|
|
}
|
|
|
|
wpm.logger.Debug().Msg("nakama: Creating watch party")
|
|
|
|
wpm.sessionCtx, wpm.sessionCtxCancel = context.WithCancel(context.Background())
|
|
|
|
// Generate unique ID
|
|
sessionID := uuid.New().String()
|
|
|
|
session := &WatchPartySession{
|
|
ID: sessionID,
|
|
Participants: make(map[string]*WatchPartySessionParticipant),
|
|
CurrentMediaInfo: nil,
|
|
Settings: options.Settings,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
// Add host as participant
|
|
session.Participants["host"] = &WatchPartySessionParticipant{
|
|
ID: "host",
|
|
Username: wpm.manager.username,
|
|
IsHost: true,
|
|
CanControl: true,
|
|
IsReady: true,
|
|
LastSeen: time.Now(),
|
|
Latency: 0,
|
|
}
|
|
|
|
wpm.currentSession = mo.Some(session)
|
|
|
|
// Reset sequence numbers for new session
|
|
wpm.sequenceMu.Lock()
|
|
wpm.sendSequence = 0
|
|
wpm.lastRxSequence = 0
|
|
wpm.sequenceMu.Unlock()
|
|
|
|
// Notify all peers about the new watch party
|
|
_ = wpm.manager.SendMessage(MessageTypeWatchPartyCreated, WatchPartyCreatedPayload{
|
|
Session: session,
|
|
})
|
|
|
|
wpm.logger.Debug().Str("sessionId", sessionID).Msg("nakama: Watch party created")
|
|
|
|
// Send websocket event to update the UI
|
|
wpm.manager.wsEventManager.SendEvent(events.NakamaWatchPartyState, session)
|
|
|
|
go func() {
|
|
ticker := time.NewTicker(5 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-wpm.sessionCtx.Done():
|
|
wpm.logger.Debug().Msg("nakama: Watch party periodic broadcast stopped")
|
|
return
|
|
case <-ticker.C:
|
|
// Broadcast the session state to all peers every 5 seconds
|
|
// This is useful for peers that will join later
|
|
wpm.broadcastSessionStateToPeers()
|
|
}
|
|
}
|
|
}()
|
|
|
|
go wpm.listenToPlaybackManager()
|
|
// go wpm.listenToOnlineStreaming() // TODO
|
|
|
|
return session, nil
|
|
}
|
|
|
|
// PromotePeerToRelayModeOrigin promotes a peer to be the origin for relay mode
|
|
func (wpm *WatchPartyManager) PromotePeerToRelayModeOrigin(peerId string) {
|
|
wpm.mu.Lock()
|
|
defer wpm.mu.Unlock()
|
|
|
|
if !wpm.manager.IsHost() {
|
|
return
|
|
}
|
|
|
|
wpm.logger.Debug().Str("peerId", peerId).Msg("nakama: Promoting peer to relay mode origin")
|
|
|
|
session, ok := wpm.currentSession.Get()
|
|
if !ok {
|
|
wpm.logger.Warn().Msg("nakama: Cannot promote peer to relay mode origin, no active watch party session")
|
|
return
|
|
}
|
|
|
|
// Check if the peer exists in the session
|
|
participant, exists := session.Participants[peerId]
|
|
if !exists {
|
|
wpm.logger.Warn().Str("peerId", peerId).Msg("nakama: Cannot promote peer to relay mode origin, peer not found in session")
|
|
return
|
|
}
|
|
|
|
// Set the IsRelayOrigin flag to true
|
|
participant.IsRelayOrigin = true
|
|
// Broadcast the updated session state to all peers
|
|
session.mu.Lock()
|
|
session.IsRelayMode = true
|
|
session.mu.Unlock()
|
|
|
|
wpm.logger.Debug().Str("peerId", peerId).Msg("nakama: Peer promoted to relay mode origin")
|
|
|
|
wpm.broadcastSessionStateToPeers()
|
|
wpm.sendSessionStateToClient()
|
|
}
|
|
|
|
func (wpm *WatchPartyManager) StopWatchParty() {
|
|
wpm.mu.Lock()
|
|
defer wpm.mu.Unlock()
|
|
|
|
if !wpm.manager.IsHost() {
|
|
return
|
|
}
|
|
|
|
wpm.logger.Debug().Msg("nakama: Stopping watch party")
|
|
|
|
// Cancel any ongoing catch-up operations
|
|
wpm.cancelCatchUp()
|
|
|
|
// Reset buffering state and cancel any waitForPeersReady goroutine
|
|
wpm.bufferMu.Lock()
|
|
wpm.isWaitingForBuffers = false
|
|
if wpm.waitForPeersCancel != nil {
|
|
wpm.waitForPeersCancel()
|
|
wpm.waitForPeersCancel = nil
|
|
}
|
|
wpm.bufferMu.Unlock()
|
|
|
|
// Broadcast the stop event to all peers
|
|
_ = wpm.manager.SendMessage(MessageTypeWatchPartyStopped, nil)
|
|
|
|
if wpm.sessionCtxCancel != nil {
|
|
wpm.sessionCtxCancel()
|
|
wpm.sessionCtx = nil
|
|
wpm.sessionCtxCancel = nil
|
|
wpm.currentSession = mo.None[*WatchPartySession]()
|
|
}
|
|
|
|
wpm.broadcastSessionStateToPeers()
|
|
wpm.sendSessionStateToClient()
|
|
}
|
|
|
|
// listenToPlaybackManager listens to the playback manager
|
|
func (wpm *WatchPartyManager) listenToPlaybackManager() {
|
|
playbackSubscriber := wpm.manager.playbackManager.SubscribeToPlaybackStatus("nakama_watch_party")
|
|
|
|
go func() {
|
|
defer util.HandlePanicInModuleThen("nakama/listenToPlaybackManager", func() {})
|
|
defer func() {
|
|
wpm.logger.Debug().Msg("nakama: Stopping playback manager listener")
|
|
go wpm.manager.playbackManager.UnsubscribeFromPlaybackStatus("nakama_watch_party")
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case <-wpm.sessionCtx.Done():
|
|
wpm.logger.Debug().Msg("nakama: Stopping playback manager listener")
|
|
return
|
|
case event := <-playbackSubscriber.EventCh:
|
|
_, ok := wpm.currentSession.Get()
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
switch event := event.(type) {
|
|
case playbackmanager.VideoStoppedEvent, playbackmanager.StreamStoppedEvent:
|
|
// Reset
|
|
wpm.logger.Debug().Msg("nakama: Playback stopped event received")
|
|
|
|
wpm.bufferMu.Lock()
|
|
wpm.isWaitingForBuffers = true
|
|
wpm.bufferWaitStart = time.Now()
|
|
// Cancel existing waitForPeersReady goroutine
|
|
if wpm.waitForPeersCancel != nil {
|
|
wpm.waitForPeersCancel()
|
|
wpm.waitForPeersCancel = nil
|
|
}
|
|
wpm.bufferMu.Unlock()
|
|
|
|
// Reset the current session media info
|
|
wpm.mu.Lock()
|
|
session, ok := wpm.currentSession.Get()
|
|
if !ok {
|
|
wpm.mu.Unlock()
|
|
return
|
|
}
|
|
session.CurrentMediaInfo = nil
|
|
wpm.mu.Unlock()
|
|
|
|
// Broadcast the session state to all peers
|
|
go wpm.broadcastSessionStateToPeers()
|
|
|
|
case playbackmanager.PlaybackStatusChangedEvent:
|
|
if event.State.MediaId == 0 {
|
|
continue
|
|
}
|
|
|
|
go func(event playbackmanager.PlaybackStatusChangedEvent) {
|
|
wpm.manager.playbackManager.PullStatus()
|
|
|
|
streamType := "file"
|
|
if event.Status.PlaybackType == mediaplayer.PlaybackTypeStream {
|
|
if strings.Contains(event.Status.Filepath, "/api/v1/torrentstream") {
|
|
streamType = "torrent"
|
|
} else {
|
|
streamType = "debrid"
|
|
}
|
|
}
|
|
|
|
optionalTorrentStreamStartOptions, _ := wpm.manager.torrentstreamRepository.GetPreviousStreamOptions()
|
|
|
|
streamPath := event.Status.Filepath
|
|
newCurrentMediaInfo := &WatchPartySessionMediaInfo{
|
|
MediaId: event.State.MediaId,
|
|
EpisodeNumber: event.State.EpisodeNumber,
|
|
AniDBEpisode: event.State.AniDbEpisode,
|
|
StreamType: streamType,
|
|
StreamPath: streamPath,
|
|
OptionalTorrentStreamStartOptions: optionalTorrentStreamStartOptions,
|
|
}
|
|
|
|
wpm.mu.Lock()
|
|
session, ok := wpm.currentSession.Get()
|
|
if !ok {
|
|
wpm.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
// If this is the same media, just send the playback status
|
|
if session.CurrentMediaInfo.Equals(newCurrentMediaInfo) && event.State.MediaId != 0 {
|
|
wpm.mu.Unlock()
|
|
|
|
// Get next sequence number for message ordering
|
|
wpm.sequenceMu.Lock()
|
|
wpm.sendSequence++
|
|
sequenceNum := wpm.sendSequence
|
|
wpm.sequenceMu.Unlock()
|
|
|
|
// Send message
|
|
_ = wpm.manager.SendMessage(MessageTypeWatchPartyPlaybackStatus, WatchPartyPlaybackStatusPayload{
|
|
PlaybackStatus: event.Status,
|
|
Timestamp: time.Now().UnixNano(),
|
|
SequenceNumber: sequenceNum,
|
|
EpisodeNumber: event.State.EpisodeNumber,
|
|
})
|
|
|
|
} else {
|
|
// For new playback, update the session
|
|
wpm.logger.Debug().Msgf("nakama: Playback changed or started: %s", streamPath)
|
|
session.CurrentMediaInfo = newCurrentMediaInfo
|
|
wpm.mu.Unlock()
|
|
|
|
// Pause immediately and wait for peers to be ready
|
|
_ = wpm.manager.playbackManager.Pause()
|
|
|
|
// Reset buffering state for new playback
|
|
wpm.bufferMu.Lock()
|
|
wpm.isWaitingForBuffers = true
|
|
wpm.bufferWaitStart = time.Now()
|
|
|
|
// Cancel existing waitForPeersReady goroutine
|
|
if wpm.waitForPeersCancel != nil {
|
|
wpm.waitForPeersCancel()
|
|
wpm.waitForPeersCancel = nil
|
|
}
|
|
wpm.bufferMu.Unlock()
|
|
|
|
go wpm.broadcastSessionStateToPeers()
|
|
|
|
// Start checking peer readiness
|
|
go wpm.waitForPeersReady(func() {
|
|
if !session.IsRelayMode {
|
|
// resume playback
|
|
_ = wpm.manager.playbackManager.Resume()
|
|
} else {
|
|
// in relay mode, just signal to the origin
|
|
_ = wpm.manager.SendMessage(MessageTypeWatchPartyRelayModePeersReady, nil)
|
|
}
|
|
})
|
|
}
|
|
}(event)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// broadcastSessionStateToPeers broadcasts the session state to all peers
|
|
func (wpm *WatchPartyManager) broadcastSessionStateToPeers() {
|
|
session, ok := wpm.currentSession.Get()
|
|
if !ok {
|
|
_ = wpm.manager.SendMessage(MessageTypeWatchPartyStateChanged, WatchPartyStateChangedPayload{
|
|
Session: nil,
|
|
})
|
|
return
|
|
}
|
|
|
|
_ = wpm.manager.SendMessage(MessageTypeWatchPartyStateChanged, WatchPartyStateChangedPayload{
|
|
Session: session,
|
|
})
|
|
}
|
|
|
|
func (wpm *WatchPartyManager) sendSessionStateToClient() {
|
|
session, ok := wpm.currentSession.Get()
|
|
if !ok {
|
|
wpm.manager.wsEventManager.SendEvent(events.NakamaWatchPartyState, nil)
|
|
return
|
|
}
|
|
|
|
wpm.manager.wsEventManager.SendEvent(events.NakamaWatchPartyState, session)
|
|
}
|
|
|
|
// handleWatchPartyPeerJoinedEvent is called when a peer joins a watch party
|
|
func (wpm *WatchPartyManager) handleWatchPartyPeerJoinedEvent(payload *WatchPartyJoinPayload, timestamp time.Time) {
|
|
if !wpm.manager.IsHost() {
|
|
return
|
|
}
|
|
|
|
wpm.mu.Lock()
|
|
defer wpm.mu.Unlock()
|
|
|
|
wpm.logger.Debug().Str("peerId", payload.PeerId).Msg("nakama: Peer joined watch party")
|
|
|
|
session, ok := wpm.currentSession.Get()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
session.mu.Lock()
|
|
// Add the peer to the session
|
|
session.Participants[payload.PeerId] = &WatchPartySessionParticipant{
|
|
ID: payload.PeerId,
|
|
Username: payload.Username,
|
|
IsHost: false,
|
|
CanControl: false,
|
|
IsReady: false,
|
|
LastSeen: timestamp,
|
|
Latency: 0,
|
|
// Initialize buffering state
|
|
IsBuffering: false,
|
|
BufferHealth: 1.0,
|
|
PlaybackStatus: nil,
|
|
}
|
|
session.mu.Unlock()
|
|
|
|
// Send session state
|
|
go wpm.broadcastSessionStateToPeers()
|
|
|
|
wpm.logger.Debug().Str("peerId", payload.PeerId).Msg("nakama: Updated watch party state after peer joined")
|
|
|
|
wpm.sendSessionStateToClient()
|
|
}
|
|
|
|
// handleWatchPartyPeerLeftEvent is called when a peer leaves a watch party
|
|
func (wpm *WatchPartyManager) handleWatchPartyPeerLeftEvent(payload *WatchPartyLeavePayload) {
|
|
if !wpm.manager.IsHost() {
|
|
return
|
|
}
|
|
|
|
wpm.mu.Lock()
|
|
defer wpm.mu.Unlock()
|
|
|
|
wpm.logger.Debug().Str("peerId", payload.PeerId).Msg("nakama: Peer left watch party")
|
|
|
|
session, ok := wpm.currentSession.Get()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Remove the peer from the session
|
|
delete(session.Participants, payload.PeerId)
|
|
|
|
// Send session state
|
|
go wpm.broadcastSessionStateToPeers()
|
|
|
|
wpm.logger.Debug().Str("peerId", payload.PeerId).Msg("nakama: Updated watch party state after peer left")
|
|
|
|
wpm.sendSessionStateToClient()
|
|
}
|
|
|
|
// HandlePeerDisconnected handles peer disconnections and removes them from the watch party
|
|
func (wpm *WatchPartyManager) HandlePeerDisconnected(peerID string) {
|
|
if !wpm.manager.IsHost() {
|
|
return
|
|
}
|
|
|
|
wpm.mu.Lock()
|
|
defer wpm.mu.Unlock()
|
|
|
|
session, ok := wpm.currentSession.Get()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Check if the peer is in the watch party
|
|
if _, exists := session.Participants[peerID]; !exists {
|
|
return
|
|
}
|
|
|
|
wpm.logger.Debug().Str("peerId", peerID).Msg("nakama: Peer disconnected, removing from watch party")
|
|
|
|
// Remove the peer from the session
|
|
delete(session.Participants, peerID)
|
|
|
|
// Send session state to remaining peers
|
|
go wpm.broadcastSessionStateToPeers()
|
|
|
|
wpm.logger.Debug().Str("peerId", peerID).Msg("nakama: Updated watch party state after peer disconnected")
|
|
|
|
// Send websocket event to update the UI
|
|
wpm.sendSessionStateToClient()
|
|
}
|
|
|
|
// handleWatchPartyPeerStatusEvent handles regular status reports from peers
|
|
func (wpm *WatchPartyManager) handleWatchPartyPeerStatusEvent(payload *WatchPartyPeerStatusPayload) {
|
|
if !wpm.manager.IsHost() {
|
|
return
|
|
}
|
|
|
|
wpm.mu.Lock()
|
|
session, ok := wpm.currentSession.Get()
|
|
if !ok {
|
|
wpm.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
// Update peer status
|
|
if participant, exists := session.Participants[payload.PeerId]; exists {
|
|
participant.PlaybackStatus = &payload.PlaybackStatus
|
|
participant.IsBuffering = payload.IsBuffering
|
|
participant.BufferHealth = payload.BufferHealth
|
|
participant.LastSeen = payload.Timestamp
|
|
participant.IsReady = !payload.IsBuffering && payload.BufferHealth > 0.1 // Consider ready if not buffering and has some buffer
|
|
|
|
wpm.logger.Debug().
|
|
Str("peerId", payload.PeerId).
|
|
Bool("isBuffering", payload.IsBuffering).
|
|
Float64("bufferHealth", payload.BufferHealth).
|
|
Bool("isReady", participant.IsReady).
|
|
Msg("nakama: Updated peer status")
|
|
}
|
|
wpm.mu.Unlock()
|
|
|
|
// Check if we should start/resume playback based on peer states (call after releasing mutex)
|
|
// Run this asynchronously to avoid blocking the event processing
|
|
go wpm.checkAndManageBuffering()
|
|
|
|
// Send session state to client to update the UI
|
|
wpm.sendSessionStateToClient()
|
|
}
|
|
|
|
// handleWatchPartyBufferUpdateEvent handles buffer state changes from peers
|
|
func (wpm *WatchPartyManager) handleWatchPartyBufferUpdateEvent(payload *WatchPartyBufferUpdatePayload) {
|
|
if !wpm.manager.IsHost() {
|
|
return
|
|
}
|
|
|
|
wpm.mu.Lock()
|
|
session, ok := wpm.currentSession.Get()
|
|
if !ok {
|
|
wpm.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
// Update peer buffer status
|
|
if participant, exists := session.Participants[payload.PeerId]; exists {
|
|
participant.IsBuffering = payload.IsBuffering
|
|
participant.BufferHealth = payload.BufferHealth
|
|
participant.LastSeen = payload.Timestamp
|
|
participant.IsReady = !payload.IsBuffering && payload.BufferHealth > 0.1
|
|
|
|
wpm.logger.Debug().
|
|
Str("peerId", payload.PeerId).
|
|
Bool("isBuffering", payload.IsBuffering).
|
|
Float64("bufferHealth", payload.BufferHealth).
|
|
Bool("isReady", participant.IsReady).
|
|
Msg("nakama: Updated peer buffer status")
|
|
}
|
|
wpm.mu.Unlock()
|
|
|
|
// Immediately check if we need to pause/resume based on buffer state (call after releasing mutex)
|
|
// Run this asynchronously to avoid blocking the event processing
|
|
go wpm.checkAndManageBuffering()
|
|
|
|
// Broadcast updated session state
|
|
go wpm.broadcastSessionStateToPeers()
|
|
|
|
// Send session state to client to update the UI
|
|
wpm.sendSessionStateToClient()
|
|
}
|
|
|
|
// checkAndManageBuffering manages playback based on peer buffering states
|
|
// NOTE: This function should NOT be called while holding wpm.mu as it may need to acquire bufferMu
|
|
func (wpm *WatchPartyManager) checkAndManageBuffering() {
|
|
session, ok := wpm.currentSession.Get()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Get current playback status
|
|
playbackStatus, hasPlayback := wpm.manager.playbackManager.PullStatus()
|
|
if !hasPlayback {
|
|
return
|
|
}
|
|
|
|
// Count peer states
|
|
var totalPeers, readyPeers, bufferingPeers int
|
|
for _, participant := range session.Participants {
|
|
if !participant.IsHost {
|
|
totalPeers++
|
|
if participant.IsReady {
|
|
readyPeers++
|
|
}
|
|
if participant.IsBuffering {
|
|
bufferingPeers++
|
|
}
|
|
}
|
|
}
|
|
|
|
// No peers means no buffering management needed
|
|
if totalPeers == 0 {
|
|
return
|
|
}
|
|
|
|
wpm.bufferMu.Lock()
|
|
defer wpm.bufferMu.Unlock()
|
|
|
|
maxWaitTime := time.Duration(session.Settings.MaxBufferWaitTime) * time.Second
|
|
|
|
// If any peer is buffering and we're playing, pause and wait
|
|
if bufferingPeers > 0 && playbackStatus.Playing {
|
|
if !wpm.isWaitingForBuffers {
|
|
wpm.logger.Debug().
|
|
Int("bufferingPeers", bufferingPeers).
|
|
Int("totalPeers", totalPeers).
|
|
Msg("nakama: Pausing playback due to peer buffering")
|
|
|
|
_ = wpm.manager.playbackManager.Pause()
|
|
wpm.isWaitingForBuffers = true
|
|
wpm.bufferWaitStart = time.Now()
|
|
}
|
|
return
|
|
}
|
|
|
|
// If we're waiting for buffers
|
|
if wpm.isWaitingForBuffers {
|
|
waitTime := time.Since(wpm.bufferWaitStart)
|
|
|
|
// Resume if all peers are ready or max wait time exceeded
|
|
if bufferingPeers == 0 || waitTime > maxWaitTime {
|
|
wpm.logger.Debug().
|
|
Int("readyPeers", readyPeers).
|
|
Int("totalPeers", totalPeers).
|
|
Int("bufferingPeers", bufferingPeers).
|
|
Float64("waitTimeSeconds", waitTime.Seconds()).
|
|
Bool("maxWaitExceeded", waitTime > maxWaitTime).
|
|
Msg("nakama: Resuming playback after buffer wait")
|
|
|
|
_ = wpm.manager.playbackManager.Resume()
|
|
wpm.isWaitingForBuffers = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// waitForPeersReady waits for peers to be ready before resuming playback
|
|
func (wpm *WatchPartyManager) waitForPeersReady(onReady func()) {
|
|
session, ok := wpm.currentSession.Get()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Create cancellable context for this goroutine
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
wpm.bufferMu.Lock()
|
|
wpm.waitForPeersCancel = cancel
|
|
wpm.bufferMu.Unlock()
|
|
|
|
defer func() {
|
|
wpm.bufferMu.Lock()
|
|
wpm.waitForPeersCancel = nil
|
|
wpm.bufferMu.Unlock()
|
|
}()
|
|
|
|
maxWaitTime := time.Duration(session.Settings.MaxBufferWaitTime) * time.Second
|
|
ticker := time.NewTicker(500 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
wpm.logger.Debug().Msg("nakama: Waiting for peers to be ready")
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
wpm.logger.Debug().Msg("nakama: waitForPeersReady cancelled")
|
|
return
|
|
case <-wpm.sessionCtx.Done():
|
|
wpm.logger.Debug().Msg("nakama: Session ended while waiting for peers")
|
|
return
|
|
case <-ticker.C:
|
|
wpm.bufferMu.Lock()
|
|
|
|
// Check if we've been waiting too long
|
|
waitTime := time.Since(wpm.bufferWaitStart)
|
|
if waitTime > maxWaitTime {
|
|
wpm.logger.Debug().Float64("waitTimeSeconds", waitTime.Seconds()).Msg("nakama: Max wait time exceeded, resuming playback")
|
|
|
|
onReady()
|
|
|
|
wpm.isWaitingForBuffers = false
|
|
wpm.bufferMu.Unlock()
|
|
return
|
|
}
|
|
|
|
// Count ready peers
|
|
session, ok := wpm.currentSession.Get()
|
|
if !ok {
|
|
wpm.bufferMu.Unlock()
|
|
return
|
|
}
|
|
|
|
var totalPeers, readyPeers int
|
|
for _, participant := range session.Participants {
|
|
if !participant.IsHost && !participant.IsRelayOrigin {
|
|
totalPeers++
|
|
if participant.IsReady {
|
|
readyPeers++
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no peers or all peers are ready, resume playback
|
|
if totalPeers == 0 || readyPeers == totalPeers {
|
|
wpm.logger.Debug().
|
|
Int("readyPeers", readyPeers).
|
|
Int("totalPeers", totalPeers).
|
|
Msg("nakama: All peers are ready, resuming playback")
|
|
|
|
onReady()
|
|
|
|
wpm.isWaitingForBuffers = false
|
|
wpm.bufferMu.Unlock()
|
|
return
|
|
}
|
|
|
|
wpm.logger.Debug().
|
|
Int("readyPeers", readyPeers).
|
|
Int("totalPeers", totalPeers).
|
|
Float64("waitTimeSeconds", waitTime.Seconds()).
|
|
Msg("nakama: Still waiting for peers to be ready")
|
|
|
|
wpm.bufferMu.Unlock()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (wpm *WatchPartyManager) EnableRelayMode(peerId string) {
|
|
wpm.mu.Lock()
|
|
defer wpm.mu.Unlock()
|
|
|
|
wpm.logger.Debug().Str("peerId", peerId).Msg("nakama: Enabling relay mode")
|
|
|
|
session, ok := wpm.currentSession.Get()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
session.mu.Lock()
|
|
participant, exists := session.Participants[peerId]
|
|
if !exists {
|
|
wpm.logger.Warn().Str("peerId", peerId).Msg("nakama: Cannot enable relay mode, peer not found in session")
|
|
wpm.manager.wsEventManager.SendEvent(events.ErrorToast, "Peer not found in session")
|
|
return
|
|
}
|
|
session.IsRelayMode = true
|
|
participant.IsRelayOrigin = true
|
|
session.mu.Unlock()
|
|
|
|
wpm.logger.Debug().Str("peerId", peerId).Msg("nakama: Relay mode enabled")
|
|
|
|
wpm.broadcastSessionStateToPeers()
|
|
wpm.sendSessionStateToClient()
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Relay mode
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// handleWatchPartyRelayModeOriginStreamStartedEvent is called when the relay origin sends us (the host) a new stream.
|
|
// It starts the same stream as the origin on the host by using the same options as the origin.
|
|
func (wpm *WatchPartyManager) handleWatchPartyRelayModeOriginStreamStartedEvent(payload *WatchPartyRelayModeOriginStreamStartedPayload) {
|
|
wpm.mu.Lock()
|
|
defer wpm.mu.Unlock()
|
|
|
|
wpm.logger.Debug().Str("filepath", payload.Filepath).Msg("nakama: Relay mode origin stream started")
|
|
|
|
session, ok := wpm.currentSession.Get()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
session.Settings.MaxBufferWaitTime = 60 // higher buffer wait time for relay mode
|
|
|
|
event := payload
|
|
|
|
// Load the stream on the host
|
|
// Playback won't actually be started
|
|
switch event.StreamType {
|
|
case "file":
|
|
// Do nothing, the file is already available
|
|
case "torrent":
|
|
// Start the torrent stream and wait for it to be ready
|
|
options := *event.OptionalTorrentStreamStartOptions
|
|
options.PlaybackType = torrentstream.PlaybackTypeNoneAndAwait
|
|
err := wpm.manager.torrentstreamRepository.StartStream(context.Background(), &options)
|
|
if err != nil {
|
|
wpm.logger.Error().Err(err).Msg("nakama: Failed to start torrent stream")
|
|
}
|
|
case "debrid":
|
|
// Start the debrid stream and wait for it to be ready
|
|
options := *event.OptionalDebridStreamStartOptions
|
|
options.PlaybackType = debrid_client.PlaybackTypeNoneAndAwait
|
|
err := wpm.manager.debridClientRepository.StartStream(context.Background(), &options)
|
|
if err != nil {
|
|
wpm.logger.Error().Err(err).Msg("nakama: Failed to start debrid stream")
|
|
}
|
|
}
|
|
|
|
// Update the current media info
|
|
streamPath := event.Status.Filepath
|
|
if event.StreamType == "file" {
|
|
// For file streams, we should use the file path directly
|
|
streamPath = event.OptionalLocalPath
|
|
}
|
|
newCurrentMediaInfo := &WatchPartySessionMediaInfo{
|
|
MediaId: event.State.MediaId,
|
|
EpisodeNumber: event.State.EpisodeNumber,
|
|
AniDBEpisode: event.State.AniDbEpisode,
|
|
StreamType: event.StreamType,
|
|
StreamPath: streamPath,
|
|
OptionalTorrentStreamStartOptions: event.OptionalTorrentStreamStartOptions,
|
|
}
|
|
|
|
// Video playback has started, send the media info to the peers
|
|
session.CurrentMediaInfo = newCurrentMediaInfo
|
|
|
|
// Pause immediately and wait for peers to be ready
|
|
_ = wpm.manager.playbackManager.Pause()
|
|
|
|
// Reset buffering state for new playback
|
|
wpm.bufferMu.Lock()
|
|
wpm.isWaitingForBuffers = true
|
|
wpm.bufferWaitStart = time.Now()
|
|
|
|
// Cancel existing waitForPeersReady goroutine
|
|
if wpm.waitForPeersCancel != nil {
|
|
wpm.waitForPeersCancel()
|
|
wpm.waitForPeersCancel = nil
|
|
}
|
|
wpm.bufferMu.Unlock()
|
|
|
|
// broadcast the session state to the peers
|
|
// this will not include the relay origin
|
|
wpm.broadcastSessionStateToPeers()
|
|
|
|
// Start checking peer readiness
|
|
go wpm.waitForPeersReady(func() {
|
|
if !session.IsRelayMode {
|
|
// not in relay mode, resume playback
|
|
_ = wpm.manager.playbackManager.Resume()
|
|
} else {
|
|
// in relay mode, just signal to the origin
|
|
_ = wpm.manager.SendMessage(MessageTypeWatchPartyRelayModePeersReady, nil)
|
|
}
|
|
})
|
|
|
|
}
|
|
|
|
// handleWatchPartyRelayModeOriginPlaybackStatusEvent is called when the relay origin sends us (the host) a playback status update
|
|
func (wpm *WatchPartyManager) handleWatchPartyRelayModeOriginPlaybackStatusEvent(payload *WatchPartyRelayModeOriginPlaybackStatusPayload) {
|
|
wpm.mu.Lock()
|
|
defer wpm.mu.Unlock()
|
|
|
|
// wpm.logger.Debug().Msg("nakama: Relay mode origin playback status")
|
|
|
|
// Send the playback status immediately to the peers
|
|
// Get next sequence number for relayed message
|
|
wpm.sequenceMu.Lock()
|
|
wpm.sendSequence++
|
|
sequenceNum := wpm.sendSequence
|
|
wpm.sequenceMu.Unlock()
|
|
|
|
_ = wpm.manager.SendMessage(MessageTypeWatchPartyPlaybackStatus, WatchPartyPlaybackStatusPayload{
|
|
PlaybackStatus: payload.Status,
|
|
Timestamp: payload.Timestamp, // timestamp of the origin
|
|
SequenceNumber: sequenceNum,
|
|
EpisodeNumber: payload.State.EpisodeNumber,
|
|
})
|
|
}
|
|
|
|
// handleWatchPartyRelayModeOriginPlaybackStoppedEvent is called when the relay origin sends us (the host) a playback stopped event
|
|
func (wpm *WatchPartyManager) handleWatchPartyRelayModeOriginPlaybackStoppedEvent() {
|
|
wpm.mu.Lock()
|
|
defer wpm.mu.Unlock()
|
|
|
|
wpm.logger.Debug().Msg("nakama: Relay mode origin playback stopped")
|
|
|
|
session, ok := wpm.currentSession.Get()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
session.mu.Lock()
|
|
session.CurrentMediaInfo = nil
|
|
session.mu.Unlock()
|
|
|
|
wpm.broadcastSessionStateToPeers()
|
|
wpm.sendSessionStateToClient()
|
|
}
|