Files
seanime-docker/seanime-2.9.10/internal/nakama/watch_party_host.go
2025-09-20 14:08:38 +01:00

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()
}