diff --git a/Emby.Server.Implementations/SyncPlay/GroupStates/PausedGroupState.cs b/Emby.Server.Implementations/SyncPlay/GroupStates/PausedGroupState.cs new file mode 100644 index 0000000000..d3bf24f747 --- /dev/null +++ b/Emby.Server.Implementations/SyncPlay/GroupStates/PausedGroupState.cs @@ -0,0 +1,171 @@ +using System.Linq; +using System; +using System.Threading; +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Class PausedGroupState. + /// + /// + /// Class is not thread-safe, external locking is required when accessing methods. + /// + public class PausedGroupState : SyncPlayAbstractState + { + /// + public override GroupState GetGroupState() + { + return GroupState.Paused; + } + + /// + public override bool HandleRequest(ISyncPlayStateContext context, bool newState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + // Change state + var playingState = new PlayingGroupState(); + context.SetState(playingState); + return playingState.HandleRequest(context, true, request, session, cancellationToken); + } + + /// + public override bool HandleRequest(ISyncPlayStateContext context, bool newState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + if (newState) + { + GroupInfo group = context.GetGroup(); + + // Pause group and compute the media playback position + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - group.LastActivity; + group.LastActivity = currentTime; + // Seek only if playback actually started + // Pause request may be issued during the delay added to account for latency + group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; + + var command = context.NewSyncPlayCommand(SendCommandType.Pause); + context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken); + } + else + { + // Client got lost, sending current state + var command = context.NewSyncPlayCommand(SendCommandType.Pause); + context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken); + } + + return true; + } + + /// + public override bool HandleRequest(ISyncPlayStateContext context, bool newState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + GroupInfo group = context.GetGroup(); + + // Sanitize PositionTicks + var ticks = context.SanitizePositionTicks(request.PositionTicks); + + // Seek + group.PositionTicks = ticks; + group.LastActivity = DateTime.UtcNow; + + var command = context.NewSyncPlayCommand(SendCommandType.Seek); + context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken); + + return true; + } + + /// + public override bool HandleRequest(ISyncPlayStateContext context, bool newState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + GroupInfo group = context.GetGroup(); + + if (newState) + { + // Pause group and compute the media playback position + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - group.LastActivity; + group.LastActivity = currentTime; + group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; + + group.SetBuffering(session, true); + + // Send pause command to all non-buffering sessions + var command = context.NewSyncPlayCommand(SendCommandType.Pause); + context.SendCommand(session, SyncPlayBroadcastType.AllReady, command, cancellationToken); + + var updateOthers = context.NewSyncPlayGroupUpdate(GroupUpdateType.GroupWait, session.UserName); + context.SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); + } + else + { + // TODO: no idea? + // group.SetBuffering(session, true); + + // Client got lost, sending current state + var command = context.NewSyncPlayCommand(SendCommandType.Pause); + context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken); + } + + return true; + } + + /// + public override bool HandleRequest(ISyncPlayStateContext context, bool newState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + GroupInfo group = context.GetGroup(); + + group.SetBuffering(session, false); + + var requestTicks = context.SanitizePositionTicks(request.PositionTicks); + + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - request.When; + var clientPosition = TimeSpan.FromTicks(requestTicks) + elapsedTime; + var delay = group.PositionTicks - clientPosition.Ticks; + + if (group.IsBuffering()) + { + // Others are still buffering, tell this client to pause when ready + var command = context.NewSyncPlayCommand(SendCommandType.Pause); + var pauseAtTime = currentTime.AddMilliseconds(delay); + command.When = context.DateToUTCString(pauseAtTime); + context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken); + } + else + { + // Let other clients resume as soon as the buffering client catches up + if (delay > group.GetHighestPing() * 2) + { + // Client that was buffering is recovering, notifying others to resume + group.LastActivity = currentTime.AddMilliseconds( + delay + ); + var command = context.NewSyncPlayCommand(SendCommandType.Play); + context.SendCommand(session, SyncPlayBroadcastType.AllExceptCurrentSession, command, cancellationToken); + } + else + { + // Client, that was buffering, resumed playback but did not update others in time + delay = Math.Max(group.GetHighestPing() * 2, group.DefaultPing); + + group.LastActivity = currentTime.AddMilliseconds( + delay + ); + + var command = context.NewSyncPlayCommand(SendCommandType.Play); + context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken); + } + + // Change state + var playingState = new PlayingGroupState(); + context.SetState(playingState); + } + + return true; + } + } +} diff --git a/Emby.Server.Implementations/SyncPlay/GroupStates/PlayingGroupState.cs b/Emby.Server.Implementations/SyncPlay/GroupStates/PlayingGroupState.cs new file mode 100644 index 0000000000..42c7779c19 --- /dev/null +++ b/Emby.Server.Implementations/SyncPlay/GroupStates/PlayingGroupState.cs @@ -0,0 +1,94 @@ +using System.Linq; +using System; +using System.Threading; +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Class PlayingGroupState. + /// + /// + /// Class is not thread-safe, external locking is required when accessing methods. + /// + public class PlayingGroupState : SyncPlayAbstractState + { + /// + public override GroupState GetGroupState() + { + return GroupState.Playing; + } + + /// + public override bool HandleRequest(ISyncPlayStateContext context, bool newState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + GroupInfo group = context.GetGroup(); + + if (newState) + { + // Pick a suitable time that accounts for latency + var delay = Math.Max(group.GetHighestPing() * 2, group.DefaultPing); + + // Unpause group and set starting point in future + // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position) + // The added delay does not guarantee, of course, that the command will be received in time + // Playback synchronization will mainly happen client side + group.LastActivity = DateTime.UtcNow.AddMilliseconds( + delay + ); + + var command = context.NewSyncPlayCommand(SendCommandType.Play); + context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken); + } + else + { + // Client got lost, sending current state + var command = context.NewSyncPlayCommand(SendCommandType.Play); + context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken); + } + + return true; + } + + /// + public override bool HandleRequest(ISyncPlayStateContext context, bool newState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + // Change state + var pausedState = new PausedGroupState(); + context.SetState(pausedState); + return pausedState.HandleRequest(context, true, request, session, cancellationToken); + } + + /// + public override bool HandleRequest(ISyncPlayStateContext context, bool newState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + // Change state + var pausedState = new PausedGroupState(); + context.SetState(pausedState); + return pausedState.HandleRequest(context, true, request, session, cancellationToken); + } + + /// + public override bool HandleRequest(ISyncPlayStateContext context, bool newState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + // Change state + var pausedState = new PausedGroupState(); + context.SetState(pausedState); + return pausedState.HandleRequest(context, true, request, session, cancellationToken); + } + + /// + public override bool HandleRequest(ISyncPlayStateContext context, bool newState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + // Group was not waiting, make sure client has latest state + var command = context.NewSyncPlayCommand(SendCommandType.Play); + context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken); + + return true; + } + } +} diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs index c98fd6d4af..225be7430d 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs @@ -8,6 +8,7 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Controller.SyncPlay; using MediaBrowser.Model.Session; using MediaBrowser.Model.SyncPlay; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.SyncPlay { @@ -17,34 +18,8 @@ namespace Emby.Server.Implementations.SyncPlay /// /// Class is not thread-safe, external locking is required when accessing methods. /// - public class SyncPlayController : ISyncPlayController + public class SyncPlayController : ISyncPlayController, ISyncPlayStateContext { - /// - /// Used to filter the sessions of a group. - /// - private enum BroadcastType - { - /// - /// All sessions will receive the message. - /// - AllGroup = 0, - - /// - /// Only the specified session will receive the message. - /// - CurrentSession = 1, - - /// - /// All sessions, except the current one, will receive the message. - /// - AllExceptCurrentSession = 2, - - /// - /// Only sessions that are not buffering will receive the message. - /// - AllReady = 3 - } - /// /// The session manager. /// @@ -55,22 +30,33 @@ namespace Emby.Server.Implementations.SyncPlay /// private readonly ISyncPlayManager _syncPlayManager; + /// + /// The logger. + /// + private readonly ILogger _logger; + /// /// The group to manage. /// private readonly GroupInfo _group = new GroupInfo(); /// - /// Initializes a new instance of the class. + /// Internal group state. /// - /// The session manager. - /// The SyncPlay manager. - public SyncPlayController( - ISessionManager sessionManager, - ISyncPlayManager syncPlayManager) + /// The group's state. + private ISyncPlayState State = new PausedGroupState(); + + /// + public GroupInfo GetGroup() { - _sessionManager = sessionManager; - _syncPlayManager = syncPlayManager; + return _group; + } + + /// + public void SetState(ISyncPlayState state) + { + _logger.LogInformation("SetState: {0} -> {1}.", State.GetGroupState(), state.GetGroupState()); + this.State = state; } /// @@ -83,13 +69,18 @@ namespace Emby.Server.Implementations.SyncPlay public bool IsGroupEmpty() => _group.IsEmpty(); /// - /// Converts DateTime to UTC string. + /// Initializes a new instance of the class. /// - /// The date to convert. - /// The UTC string. - private string DateToUTCString(DateTime date) + /// The session manager. + /// The SyncPlay manager. + public SyncPlayController( + ISessionManager sessionManager, + ISyncPlayManager syncPlayManager, + ILogger logger) { - return date.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture); + _sessionManager = sessionManager; + _syncPlayManager = syncPlayManager; + _logger = logger; } /// @@ -98,37 +89,30 @@ namespace Emby.Server.Implementations.SyncPlay /// The current session. /// The filtering type. /// The array of sessions matching the filter. - private IEnumerable FilterSessions(SessionInfo from, BroadcastType type) + private SessionInfo[] FilterSessions(SessionInfo from, SyncPlayBroadcastType type) { switch (type) { - case BroadcastType.CurrentSession: + case SyncPlayBroadcastType.CurrentSession: return new SessionInfo[] { from }; - case BroadcastType.AllGroup: - return _group.Participants.Values - .Select(session => session.Session); - case BroadcastType.AllExceptCurrentSession: - return _group.Participants.Values - .Select(session => session.Session) - .Where(session => !session.Id.Equals(from.Id, StringComparison.Ordinal)); - case BroadcastType.AllReady: - return _group.Participants.Values - .Where(session => !session.IsBuffering) - .Select(session => session.Session); + case SyncPlayBroadcastType.AllGroup: + return _group.Participants.Values.Select( + session => session.Session).ToArray(); + case SyncPlayBroadcastType.AllExceptCurrentSession: + return _group.Participants.Values.Select( + session => session.Session).Where( + session => !session.Id.Equals(from.Id)).ToArray(); + case SyncPlayBroadcastType.AllReady: + return _group.Participants.Values.Where( + session => !session.IsBuffering).Select( + session => session.Session).ToArray(); default: return Array.Empty(); } } - /// - /// Sends a GroupUpdate message to the interested sessions. - /// - /// The current session. - /// The filtering type. - /// The message to send. - /// The cancellation token. - /// The task. - private Task SendGroupUpdate(SessionInfo from, BroadcastType type, GroupUpdate message, CancellationToken cancellationToken) + /// + public Task SendGroupUpdate(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate message, CancellationToken cancellationToken) { IEnumerable GetTasks() { @@ -141,15 +125,8 @@ namespace Emby.Server.Implementations.SyncPlay return Task.WhenAll(GetTasks()); } - /// - /// Sends a playback command to the interested sessions. - /// - /// The current session. - /// The filtering type. - /// The message to send. - /// The cancellation token. - /// The task. - private Task SendCommand(SessionInfo from, BroadcastType type, SendCommand message, CancellationToken cancellationToken) + /// + public Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken cancellationToken) { IEnumerable GetTasks() { @@ -162,12 +139,8 @@ namespace Emby.Server.Implementations.SyncPlay return Task.WhenAll(GetTasks()); } - /// - /// Builds a new playback command with some default values. - /// - /// The command type. - /// The SendCommand. - private SendCommand NewSyncPlayCommand(SendCommandType type) + /// + public SendCommand NewSyncPlayCommand(SendCommandType type) { return new SendCommand() { @@ -179,13 +152,8 @@ namespace Emby.Server.Implementations.SyncPlay }; } - /// - /// Builds a new group update message. - /// - /// The update type. - /// The data to send. - /// The GroupUpdate. - private GroupUpdate NewSyncPlayGroupUpdate(GroupUpdateType type, T data) + /// + public GroupUpdate NewSyncPlayGroupUpdate(GroupUpdateType type, T data) { return new GroupUpdate() { @@ -196,285 +164,13 @@ namespace Emby.Server.Implementations.SyncPlay } /// - public void CreateGroup(SessionInfo session, CancellationToken cancellationToken) + public string DateToUTCString(DateTime _date) { - _group.AddSession(session); - _syncPlayManager.AddSessionToGroup(session, this); - - _group.PlayingItem = session.FullNowPlayingItem; - _group.IsPaused = session.PlayState.IsPaused; - _group.PositionTicks = session.PlayState.PositionTicks ?? 0; - _group.LastActivity = DateTime.UtcNow; - - var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); - SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); + return _date.ToUniversalTime().ToString("o"); } /// - public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken) - { - if (session.NowPlayingItem?.Id == _group.PlayingItem.Id) - { - _group.AddSession(session); - _syncPlayManager.AddSessionToGroup(session, this); - - var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); - SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); - - var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName); - SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); - - // Syncing will happen client-side - if (!_group.IsPaused) - { - var playCommand = NewSyncPlayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.CurrentSession, playCommand, cancellationToken); - } - else - { - var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken); - } - } - else - { - var playRequest = new PlayRequest - { - ItemIds = new Guid[] { _group.PlayingItem.Id }, - StartPositionTicks = _group.PositionTicks - }; - var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest); - SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken); - } - } - - /// - public void SessionLeave(SessionInfo session, CancellationToken cancellationToken) - { - _group.RemoveSession(session); - _syncPlayManager.RemoveSessionFromGroup(session, this); - - var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupLeft, _group.PositionTicks); - SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); - - var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserLeft, session.UserName); - SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); - } - - /// - public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - // The server's job is to maintain a consistent state for clients to reference - // and notify clients of state changes. The actual syncing of media playback - // happens client side. Clients are aware of the server's time and use it to sync. - switch (request.Type) - { - case PlaybackRequestType.Play: - HandlePlayRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.Pause: - HandlePauseRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.Seek: - HandleSeekRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.Buffer: - HandleBufferingRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.Ready: - HandleBufferingDoneRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.Ping: - HandlePingUpdateRequest(session, request); - break; - } - } - - /// - /// Handles a play action requested by a session. - /// - /// The session. - /// The play action. - /// The cancellation token. - private void HandlePlayRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - if (_group.IsPaused) - { - // Pick a suitable time that accounts for latency - var delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing); - - // Unpause group and set starting point in future - // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position) - // The added delay does not guarantee, of course, that the command will be received in time - // Playback synchronization will mainly happen client side - _group.IsPaused = false; - _group.LastActivity = DateTime.UtcNow.AddMilliseconds( - delay); - - var command = NewSyncPlayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); - } - else - { - // Client got lost, sending current state - var command = NewSyncPlayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - } - - /// - /// Handles a pause action requested by a session. - /// - /// The session. - /// The pause action. - /// The cancellation token. - private void HandlePauseRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - if (!_group.IsPaused) - { - // Pause group and compute the media playback position - _group.IsPaused = true; - var currentTime = DateTime.UtcNow; - var elapsedTime = currentTime - _group.LastActivity; - _group.LastActivity = currentTime; - - // Seek only if playback actually started - // Pause request may be issued during the delay added to account for latency - _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; - - var command = NewSyncPlayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); - } - else - { - // Client got lost, sending current state - var command = NewSyncPlayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - } - - /// - /// Handles a seek action requested by a session. - /// - /// The session. - /// The seek action. - /// The cancellation token. - private void HandleSeekRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - // Sanitize PositionTicks - var ticks = SanitizePositionTicks(request.PositionTicks); - - // Pause and seek - _group.IsPaused = true; - _group.PositionTicks = ticks; - _group.LastActivity = DateTime.UtcNow; - - var command = NewSyncPlayCommand(SendCommandType.Seek); - SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); - } - - /// - /// Handles a buffering action requested by a session. - /// - /// The session. - /// The buffering action. - /// The cancellation token. - private void HandleBufferingRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - if (!_group.IsPaused) - { - // Pause group and compute the media playback position - _group.IsPaused = true; - var currentTime = DateTime.UtcNow; - var elapsedTime = currentTime - _group.LastActivity; - _group.LastActivity = currentTime; - _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; - - _group.SetBuffering(session, true); - - // Send pause command to all non-buffering sessions - var command = NewSyncPlayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.AllReady, command, cancellationToken); - - var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.GroupWait, session.UserName); - SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); - } - else - { - // Client got lost, sending current state - var command = NewSyncPlayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - } - - /// - /// Handles a buffering-done action requested by a session. - /// - /// The session. - /// The buffering-done action. - /// The cancellation token. - private void HandleBufferingDoneRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - if (_group.IsPaused) - { - _group.SetBuffering(session, false); - - var requestTicks = SanitizePositionTicks(request.PositionTicks); - - var when = request.When ?? DateTime.UtcNow; - var currentTime = DateTime.UtcNow; - var elapsedTime = currentTime - when; - var clientPosition = TimeSpan.FromTicks(requestTicks) + elapsedTime; - var delay = _group.PositionTicks - clientPosition.Ticks; - - if (_group.IsBuffering()) - { - // Others are still buffering, tell this client to pause when ready - var command = NewSyncPlayCommand(SendCommandType.Pause); - var pauseAtTime = currentTime.AddMilliseconds(delay); - command.When = DateToUTCString(pauseAtTime); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - else - { - // Let other clients resume as soon as the buffering client catches up - _group.IsPaused = false; - - if (delay > _group.GetHighestPing() * 2) - { - // Client that was buffering is recovering, notifying others to resume - _group.LastActivity = currentTime.AddMilliseconds( - delay); - var command = NewSyncPlayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.AllExceptCurrentSession, command, cancellationToken); - } - else - { - // Client, that was buffering, resumed playback but did not update others in time - delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing); - - _group.LastActivity = currentTime.AddMilliseconds( - delay); - - var command = NewSyncPlayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); - } - } - } - else - { - // Group was not waiting, make sure client has latest state - var command = NewSyncPlayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - } - - /// - /// Sanitizes the PositionTicks, considers the current playing item when available. - /// - /// The PositionTicks. - /// The sanitized PositionTicks. - private long SanitizePositionTicks(long? positionTicks) + public long SanitizePositionTicks(long? positionTicks) { var ticks = positionTicks ?? 0; ticks = ticks >= 0 ? ticks : 0; @@ -487,15 +183,87 @@ namespace Emby.Server.Implementations.SyncPlay return ticks; } - /// - /// Updates ping of a session. - /// - /// The session. - /// The update. - private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request) + /// + public void CreateGroup(SessionInfo session, CancellationToken cancellationToken) { - // Collected pings are used to account for network latency when unpausing playback - _group.UpdatePing(session, request.Ping ?? GroupInfo.DefaultPing); + _group.AddSession(session); + _syncPlayManager.AddSessionToGroup(session, this); + + State = new PausedGroupState(); + + _group.PlayingItem = session.FullNowPlayingItem; + // TODO: looks like new groups should mantain playstate (and not force to pause) + // _group.IsPaused = session.PlayState.IsPaused; + _group.PositionTicks = session.PlayState.PositionTicks ?? 0; + _group.LastActivity = DateTime.UtcNow; + + var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); + SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); + // TODO: looks like new groups should mantain playstate (and not force to pause) + var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, SyncPlayBroadcastType.CurrentSession, pauseCommand, cancellationToken); + } + + /// + public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken) + { + if (session.NowPlayingItem?.Id == _group.PlayingItem.Id) + { + _group.AddSession(session); + _syncPlayManager.AddSessionToGroup(session, this); + + var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); + SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); + + var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName); + SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); + + // Syncing will happen client-side + if (State.GetGroupState().Equals(GroupState.Playing)) + { + var playCommand = NewSyncPlayCommand(SendCommandType.Play); + SendCommand(session, SyncPlayBroadcastType.CurrentSession, playCommand, cancellationToken); + } + else + { + var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, SyncPlayBroadcastType.CurrentSession, pauseCommand, cancellationToken); + } + } + else + { + var playRequest = new PlayRequest + { + ItemIds = new Guid[] { _group.PlayingItem.Id }, + StartPositionTicks = _group.PositionTicks + }; + var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest); + SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, update, cancellationToken); + } + } + + /// + public void SessionLeave(SessionInfo session, CancellationToken cancellationToken) + { + _group.RemoveSession(session); + _syncPlayManager.RemoveSessionFromGroup(session, this); + + var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupLeft, _group.PositionTicks); + SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); + + var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserLeft, session.UserName); + SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); + } + + /// + public void HandleRequest(SessionInfo session, IPlaybackGroupRequest request, CancellationToken cancellationToken) + { + // The server's job is to maintain a consistent state for clients to reference + // and notify clients of state changes. The actual syncing of media playback + // happens client side. Clients are aware of the server's time and use it to sync. + _logger.LogInformation("HandleRequest: {0}:{1}.", request.GetType(), State.GetGroupState()); + _ = request.Apply(this, State, session, cancellationToken); + // TODO: do something with returned value } /// diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs index 550939d701..b85f3c1496 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -186,7 +186,7 @@ namespace Emby.Server.Implementations.SyncPlay LeaveGroup(session, cancellationToken); } - var group = new SyncPlayController(_sessionManager, this); + var group = new SyncPlayController(_sessionManager, this, _logger); _groups[group.GetGroupId()] = group; group.CreateGroup(session, cancellationToken); @@ -312,7 +312,7 @@ namespace Emby.Server.Implementations.SyncPlay } /// - public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + public void HandleRequest(SessionInfo session, IPlaybackGroupRequest request, CancellationToken cancellationToken) { var user = _userManager.GetUserById(session.UserId); diff --git a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs index 6035f84e4a..bd9670f07a 100644 --- a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs +++ b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs @@ -172,7 +172,7 @@ namespace MediaBrowser.Api.SyncPlay /// /// The request. /// The requested list of groups. - public List Get(SyncPlayListGroups request) + public List Get(SyncPlayList request) { var currentSession = GetSession(_sessionContext); var filterItemId = Guid.Empty; @@ -192,10 +192,7 @@ namespace MediaBrowser.Api.SyncPlay public void Post(SyncPlayPlay request) { var currentSession = GetSession(_sessionContext); - var syncPlayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.Play - }; + var syncPlayRequest = new PlayGroupRequest(); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); } @@ -206,10 +203,7 @@ namespace MediaBrowser.Api.SyncPlay public void Post(SyncPlayPause request) { var currentSession = GetSession(_sessionContext); - var syncPlayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.Pause - }; + var syncPlayRequest = new PauseGroupRequest(); _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); } @@ -220,9 +214,8 @@ namespace MediaBrowser.Api.SyncPlay public void Post(SyncPlaySeek request) { var currentSession = GetSession(_sessionContext); - var syncPlayRequest = new PlaybackRequest() + var syncPlayRequest = new SeekGroupRequest() { - Type = PlaybackRequestType.Seek, PositionTicks = request.PositionTicks }; _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); @@ -235,12 +228,25 @@ namespace MediaBrowser.Api.SyncPlay public void Post(SyncPlayBuffering request) { var currentSession = GetSession(_sessionContext); - var syncPlayRequest = new PlaybackRequest() + + IPlaybackGroupRequest syncPlayRequest; + if (!request.BufferingDone) { - Type = request.BufferingDone ? PlaybackRequestType.Ready : PlaybackRequestType.Buffer, - When = DateTime.Parse(request.When), - PositionTicks = request.PositionTicks - }; + syncPlayRequest = new BufferGroupRequest() + { + When = DateTime.Parse(request.When), + PositionTicks = request.PositionTicks + }; + } + else + { + syncPlayRequest = new ReadyGroupRequest() + { + When = DateTime.Parse(request.When), + PositionTicks = request.PositionTicks + }; + } + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); } @@ -251,9 +257,8 @@ namespace MediaBrowser.Api.SyncPlay public void Post(SyncPlayPing request) { var currentSession = GetSession(_sessionContext); - var syncPlayRequest = new PlaybackRequest() + var syncPlayRequest = new PingGroupRequest() { - Type = PlaybackRequestType.Ping, Ping = Convert.ToInt64(request.Ping) }; _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); diff --git a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs index a1cada25cc..cdd24d0b59 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs @@ -30,12 +30,6 @@ namespace MediaBrowser.Controller.SyncPlay /// The playing item. public BaseItem PlayingItem { get; set; } - /// - /// Gets or sets a value indicating whether playback is paused. - /// - /// Playback is paused. - public bool IsPaused { get; set; } - /// /// Gets or sets a value indicating whether there are position ticks. /// diff --git a/MediaBrowser.Controller/SyncPlay/IPlaybackGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/IPlaybackGroupRequest.cs new file mode 100644 index 0000000000..a6e87a007f --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/IPlaybackGroupRequest.cs @@ -0,0 +1,24 @@ +using System.Threading; +using MediaBrowser.Model.SyncPlay; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Interface IPlaybackGroupRequest. + /// + public interface IPlaybackGroupRequest + { + /// + /// Gets the playback request type. + /// + /// The playback request type. + PlaybackRequestType Type(); + + /// + /// Applies the request to a group. + /// + /// The operation completion status. + bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs index d869c05bd1..5ac2aeb247 100644 --- a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs @@ -56,7 +56,7 @@ namespace MediaBrowser.Controller.SyncPlay /// The session. /// The requested action. /// The cancellation token. - void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken); + void HandleRequest(SessionInfo session, IPlaybackGroupRequest request, CancellationToken cancellationToken); /// /// Gets the info about the group for the clients. diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs index 65770021dc..6fa94e2ce4 100644 --- a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs @@ -48,7 +48,7 @@ namespace MediaBrowser.Controller.SyncPlay /// The session. /// The request. /// The cancellation token. - void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken); + void HandleRequest(SessionInfo session, IPlaybackGroupRequest request, CancellationToken cancellationToken); /// /// Maps a session to a group. diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayState.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayState.cs new file mode 100644 index 0000000000..55c9ee938f --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayState.cs @@ -0,0 +1,95 @@ +using System.Threading; +using MediaBrowser.Model.SyncPlay; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Interface ISyncPlayState. + /// + public interface ISyncPlayState + { + /// + /// Gets the group state. + /// + /// The group state. + GroupState GetGroupState(); + + /// + /// Generic handle. Context's state can change. + /// + /// The context of the state. + /// Whether the state has been just set. + /// The play action. + /// The session. + /// The cancellation token. + /// The operation completion status. + bool HandleRequest(ISyncPlayStateContext context, bool newState, IPlaybackGroupRequest request, SessionInfo session, CancellationToken cancellationToken); + + /// + /// Handles a play action requested by a session. Context's state can change. + /// + /// The context of the state. + /// Whether the state has been just set. + /// The play action. + /// The session. + /// The cancellation token. + /// The operation completion status. + bool HandleRequest(ISyncPlayStateContext context, bool newState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken); + + /// + /// Handles a pause action requested by a session. Context's state can change. + /// + /// The context of the state. + /// Whether the state has been just set. + /// The pause action. + /// The session. + /// The cancellation token. + /// The operation completion status. + bool HandleRequest(ISyncPlayStateContext context, bool newState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken); + + /// + /// Handles a seek action requested by a session. Context's state can change. + /// + /// The context of the state. + /// Whether the state has been just set. + /// The seek action. + /// The session. + /// The cancellation token. + /// The operation completion status. + bool HandleRequest(ISyncPlayStateContext context, bool newState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken); + + /// + /// Handles a buffering action requested by a session. Context's state can change. + /// + /// The context of the state. + /// Whether the state has been just set. + /// The buffering action. + /// The session. + /// The cancellation token. + /// The operation completion status. + bool HandleRequest(ISyncPlayStateContext context, bool newState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken); + + /// + /// Handles a buffering-done action requested by a session. Context's state can change. + /// + /// The context of the state. + /// Whether the state has been just set. + /// The buffering-done action. + /// The session. + /// The cancellation token. + /// The operation completion status. + bool HandleRequest(ISyncPlayStateContext context, bool newState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken); + + /// + /// Updates ping of a session. Context's state should not change. + /// + /// The context of the state. + /// Whether the state has been just set. + /// The buffering-done action. + /// The session. + /// The cancellation token. + /// The operation completion status. + bool HandleRequest(ISyncPlayStateContext context, bool newState, PingGroupRequest request, SessionInfo session, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayStateContext.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayStateContext.cs new file mode 100644 index 0000000000..9bdb1ace6c --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayStateContext.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.SyncPlay; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Interface ISyncPlayStateContext. + /// + public interface ISyncPlayStateContext + { + /// + /// Gets the context's group. + /// + /// The group. + GroupInfo GetGroup(); + + /// + /// Sets a new state. + /// + /// The new state. + void SetState(ISyncPlayState state); + + /// + /// Sends a GroupUpdate message to the interested sessions. + /// + /// The current session. + /// The filtering type. + /// The message to send. + /// The cancellation token. + /// The task. + Task SendGroupUpdate(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate message, CancellationToken cancellationToken); + + /// + /// Sends a playback command to the interested sessions. + /// + /// The current session. + /// The filtering type. + /// The message to send. + /// The cancellation token. + /// The task. + Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken cancellationToken); + + /// + /// Builds a new playback command with some default values. + /// + /// The command type. + /// The SendCommand. + SendCommand NewSyncPlayCommand(SendCommandType type); + + /// + /// Builds a new group update message. + /// + /// The update type. + /// The data to send. + /// The GroupUpdate. + GroupUpdate NewSyncPlayGroupUpdate(GroupUpdateType type, T data); + + /// + /// Converts DateTime to UTC string. + /// + /// The date to convert. + /// The UTC string. + string DateToUTCString(DateTime date); + + /// + /// Sanitizes the PositionTicks, considers the current playing item when available. + /// + /// The PositionTicks. + /// The sanitized PositionTicks. + long SanitizePositionTicks(long? positionTicks); + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/BufferGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/BufferGroupRequest.cs new file mode 100644 index 0000000000..21dae8e4e6 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/BufferGroupRequest.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; +using MediaBrowser.Model.SyncPlay; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Class BufferingGroupRequest. + /// + public class BufferGroupRequest : IPlaybackGroupRequest + { + /// + /// Gets or sets when the request has been made by the client. + /// + /// The date of the request. + public DateTime When { get; set; } + + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + public long PositionTicks { get; set; } + + /// + /// Gets or sets the playing item id. + /// + /// The playing item id. + public Guid PlayingItemId { get; set; } + + /// + public PlaybackRequestType Type() + { + return PlaybackRequestType.Buffer; + } + + /// + public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken) + { + return state.HandleRequest(context, false, this, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PauseGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PauseGroupRequest.cs new file mode 100644 index 0000000000..21a46add8c --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PauseGroupRequest.cs @@ -0,0 +1,24 @@ +using System.Threading; +using MediaBrowser.Model.SyncPlay; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Class PauseGroupRequest. + /// + public class PauseGroupRequest : IPlaybackGroupRequest + { + /// + public PlaybackRequestType Type() + { + return PlaybackRequestType.Pause; + } + + /// + public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken) + { + return state.HandleRequest(context, false, this, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PingGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PingGroupRequest.cs new file mode 100644 index 0000000000..2f78edfc56 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PingGroupRequest.cs @@ -0,0 +1,31 @@ +using System.Threading; +using MediaBrowser.Model.SyncPlay; +using MediaBrowser.Controller.Session; + +// FIXME: not really group related, can be moved up to SyncPlayController maybe? +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Class UpdatePingGroupRequest. + /// + public class PingGroupRequest : IPlaybackGroupRequest + { + /// + /// Gets or sets the ping time. + /// + /// The ping time. + public long Ping { get; set; } + + /// + public PlaybackRequestType Type() + { + return PlaybackRequestType.Ping; + } + + /// + public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken) + { + return state.HandleRequest(context, false, this, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PlayGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PlayGroupRequest.cs new file mode 100644 index 0000000000..942229a775 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PlayGroupRequest.cs @@ -0,0 +1,24 @@ +using System.Threading; +using MediaBrowser.Model.SyncPlay; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Class PlayGroupRequest. + /// + public class PlayGroupRequest : IPlaybackGroupRequest + { + /// + public PlaybackRequestType Type() + { + return PlaybackRequestType.Play; + } + + /// + public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken) + { + return state.HandleRequest(context, false, this, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/ReadyGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/ReadyGroupRequest.cs new file mode 100644 index 0000000000..ee88ddddbb --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/ReadyGroupRequest.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; +using MediaBrowser.Model.SyncPlay; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Class BufferingDoneGroupRequest. + /// + public class ReadyGroupRequest : IPlaybackGroupRequest + { + /// + /// Gets or sets when the request has been made by the client. + /// + /// The date of the request. + public DateTime When { get; set; } + + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + public long PositionTicks { get; set; } + + /// + /// Gets or sets the playing item id. + /// + /// The playing item id. + public Guid PlayingItemId { get; set; } + + /// + public PlaybackRequestType Type() + { + return PlaybackRequestType.Ready; + } + + /// + public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken) + { + return state.HandleRequest(context, false, this, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SeekGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SeekGroupRequest.cs new file mode 100644 index 0000000000..bb5e7a343e --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SeekGroupRequest.cs @@ -0,0 +1,30 @@ +using System.Threading; +using MediaBrowser.Model.SyncPlay; +using MediaBrowser.Controller.Session; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Class SeekGroupRequest. + /// + public class SeekGroupRequest : IPlaybackGroupRequest + { + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + public long PositionTicks { get; set; } + + /// + public PlaybackRequestType Type() + { + return PlaybackRequestType.Seek; + } + + /// + public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken) + { + return state.HandleRequest(context, false, this, session, cancellationToken); + } + } +} diff --git a/MediaBrowser.Controller/SyncPlay/SyncPlayAbstractState.cs b/MediaBrowser.Controller/SyncPlay/SyncPlayAbstractState.cs new file mode 100644 index 0000000000..0b72d16686 --- /dev/null +++ b/MediaBrowser.Controller/SyncPlay/SyncPlayAbstractState.cs @@ -0,0 +1,65 @@ +using System.Threading; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.SyncPlay; + +namespace MediaBrowser.Controller.SyncPlay +{ + /// + /// Class SyncPlayAbstractState. + /// + /// + /// Class is not thread-safe, external locking is required when accessing methods. + /// + public abstract class SyncPlayAbstractState : ISyncPlayState + { + /// + public abstract GroupState GetGroupState(); + + /// + public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, IPlaybackGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + return true; + } + + /// + public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + return true; + } + + /// + public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + return true; + } + + /// + public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + return true; + } + + /// + public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + return true; + } + + /// + public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + return true; + } + + /// + public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, PingGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + GroupInfo group = context.GetGroup(); + + // Collected pings are used to account for network latency when unpausing playback + group.UpdatePing(session, request.Ping); + + return true; + } + } +} diff --git a/MediaBrowser.Model/SyncPlay/GroupState.cs b/MediaBrowser.Model/SyncPlay/GroupState.cs new file mode 100644 index 0000000000..871634d558 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/GroupState.cs @@ -0,0 +1,25 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Enum GroupState. + /// + public enum GroupState + { + /// + /// The group is in idle state. No media is playing. + /// + Idle, + /// + /// The group is in wating state. Playback is paused. Will start playing when users are ready. + /// + Waiting, + /// + /// The group is in paused state. Playback is paused. Will resume on play command. + /// + Paused, + /// + /// The group is in playing state. Playback is advancing. + /// + Playing + } +} diff --git a/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs b/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs deleted file mode 100644 index 9de23194e3..0000000000 --- a/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; - -namespace MediaBrowser.Model.SyncPlay -{ - /// - /// Class PlaybackRequest. - /// - public class PlaybackRequest - { - /// - /// Gets or sets the request type. - /// - /// The request type. - public PlaybackRequestType Type { get; set; } - - /// - /// Gets or sets when the request has been made by the client. - /// - /// The date of the request. - public DateTime? When { get; set; } - - /// - /// Gets or sets the position ticks. - /// - /// The position ticks. - public long? PositionTicks { get; set; } - - /// - /// Gets or sets the ping time. - /// - /// The ping time. - public long? Ping { get; set; } - } -} diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayBroadcastType.cs b/MediaBrowser.Model/SyncPlay/SyncPlayBroadcastType.cs new file mode 100644 index 0000000000..29dbb11b38 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/SyncPlayBroadcastType.cs @@ -0,0 +1,28 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// + /// Used to filter the sessions of a group. + /// + public enum SyncPlayBroadcastType + { + /// + /// All sessions will receive the message. + /// + AllGroup = 0, + + /// + /// Only the specified session will receive the message. + /// + CurrentSession = 1, + + /// + /// All sessions, except the current one, will receive the message. + /// + AllExceptCurrentSession = 2, + + /// + /// Only sessions that are not buffering will receive the message. + /// + AllReady = 3 + } +}