diff options
21 files changed, 891 insertions, 386 deletions
diff --git a/Emby.Server.Implementations/SyncPlay/GroupStates/PausedGroupState.cs b/Emby.Server.Implementations/SyncPlay/GroupStates/PausedGroupState.cs new file mode 100644 index 000000000..d3bf24f74 --- /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 +{ + /// <summary> + /// Class PausedGroupState. + /// </summary> + /// <remarks> + /// Class is not thread-safe, external locking is required when accessing methods. + /// </remarks> + public class PausedGroupState : SyncPlayAbstractState + { + /// <inheritdoc /> + public override GroupState GetGroupState() + { + return GroupState.Paused; + } + + /// <inheritdoc /> + 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); + } + + /// <inheritdoc /> + 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; + } + + /// <inheritdoc /> + 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; + } + + /// <inheritdoc /> + 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; + } + + /// <inheritdoc /> + 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 000000000..42c7779c1 --- /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 +{ + /// <summary> + /// Class PlayingGroupState. + /// </summary> + /// <remarks> + /// Class is not thread-safe, external locking is required when accessing methods. + /// </remarks> + public class PlayingGroupState : SyncPlayAbstractState + { + /// <inheritdoc /> + public override GroupState GetGroupState() + { + return GroupState.Playing; + } + + /// <inheritdoc /> + 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; + } + + /// <inheritdoc /> + 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); + } + + /// <inheritdoc /> + 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); + } + + /// <inheritdoc /> + 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); + } + + /// <inheritdoc /> + 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 c98fd6d4a..225be7430 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,35 +18,9 @@ namespace Emby.Server.Implementations.SyncPlay /// <remarks> /// Class is not thread-safe, external locking is required when accessing methods. /// </remarks> - public class SyncPlayController : ISyncPlayController + public class SyncPlayController : ISyncPlayController, ISyncPlayStateContext { /// <summary> - /// Used to filter the sessions of a group. - /// </summary> - private enum BroadcastType - { - /// <summary> - /// All sessions will receive the message. - /// </summary> - AllGroup = 0, - - /// <summary> - /// Only the specified session will receive the message. - /// </summary> - CurrentSession = 1, - - /// <summary> - /// All sessions, except the current one, will receive the message. - /// </summary> - AllExceptCurrentSession = 2, - - /// <summary> - /// Only sessions that are not buffering will receive the message. - /// </summary> - AllReady = 3 - } - - /// <summary> /// The session manager. /// </summary> private readonly ISessionManager _sessionManager; @@ -56,21 +31,32 @@ namespace Emby.Server.Implementations.SyncPlay private readonly ISyncPlayManager _syncPlayManager; /// <summary> + /// The logger. + /// </summary> + private readonly ILogger _logger; + + /// <summary> /// The group to manage. /// </summary> private readonly GroupInfo _group = new GroupInfo(); /// <summary> - /// Initializes a new instance of the <see cref="SyncPlayController" /> class. + /// Internal group state. /// </summary> - /// <param name="sessionManager">The session manager.</param> - /// <param name="syncPlayManager">The SyncPlay manager.</param> - public SyncPlayController( - ISessionManager sessionManager, - ISyncPlayManager syncPlayManager) + /// <value>The group's state.</value> + private ISyncPlayState State = new PausedGroupState(); + + /// <inheritdoc /> + public GroupInfo GetGroup() { - _sessionManager = sessionManager; - _syncPlayManager = syncPlayManager; + return _group; + } + + /// <inheritdoc /> + public void SetState(ISyncPlayState state) + { + _logger.LogInformation("SetState: {0} -> {1}.", State.GetGroupState(), state.GetGroupState()); + this.State = state; } /// <inheritdoc /> @@ -83,13 +69,18 @@ namespace Emby.Server.Implementations.SyncPlay public bool IsGroupEmpty() => _group.IsEmpty(); /// <summary> - /// Converts DateTime to UTC string. + /// Initializes a new instance of the <see cref="SyncPlayController" /> class. /// </summary> - /// <param name="date">The date to convert.</param> - /// <value>The UTC string.</value> - private string DateToUTCString(DateTime date) + /// <param name="sessionManager">The session manager.</param> + /// <param name="syncPlayManager">The SyncPlay manager.</param> + public SyncPlayController( + ISessionManager sessionManager, + ISyncPlayManager syncPlayManager, + ILogger logger) { - return date.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture); + _sessionManager = sessionManager; + _syncPlayManager = syncPlayManager; + _logger = logger; } /// <summary> @@ -98,37 +89,30 @@ namespace Emby.Server.Implementations.SyncPlay /// <param name="from">The current session.</param> /// <param name="type">The filtering type.</param> /// <value>The array of sessions matching the filter.</value> - private IEnumerable<SessionInfo> 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<SessionInfo>(); } } - /// <summary> - /// Sends a GroupUpdate message to the interested sessions. - /// </summary> - /// <param name="from">The current session.</param> - /// <param name="type">The filtering type.</param> - /// <param name="message">The message to send.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <value>The task.</value> - private Task SendGroupUpdate<T>(SessionInfo from, BroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken) + /// <inheritdoc /> + public Task SendGroupUpdate<T>(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken) { IEnumerable<Task> GetTasks() { @@ -141,15 +125,8 @@ namespace Emby.Server.Implementations.SyncPlay return Task.WhenAll(GetTasks()); } - /// <summary> - /// Sends a playback command to the interested sessions. - /// </summary> - /// <param name="from">The current session.</param> - /// <param name="type">The filtering type.</param> - /// <param name="message">The message to send.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <value>The task.</value> - private Task SendCommand(SessionInfo from, BroadcastType type, SendCommand message, CancellationToken cancellationToken) + /// <inheritdoc /> + public Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken cancellationToken) { IEnumerable<Task> GetTasks() { @@ -162,12 +139,8 @@ namespace Emby.Server.Implementations.SyncPlay return Task.WhenAll(GetTasks()); } - /// <summary> - /// Builds a new playback command with some default values. - /// </summary> - /// <param name="type">The command type.</param> - /// <value>The SendCommand.</value> - private SendCommand NewSyncPlayCommand(SendCommandType type) + /// <inheritdoc /> + public SendCommand NewSyncPlayCommand(SendCommandType type) { return new SendCommand() { @@ -179,13 +152,8 @@ namespace Emby.Server.Implementations.SyncPlay }; } - /// <summary> - /// Builds a new group update message. - /// </summary> - /// <param name="type">The update type.</param> - /// <param name="data">The data to send.</param> - /// <value>The GroupUpdate.</value> - private GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data) + /// <inheritdoc /> + public GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data) { return new GroupUpdate<T>() { @@ -196,18 +164,44 @@ namespace Emby.Server.Implementations.SyncPlay } /// <inheritdoc /> + public string DateToUTCString(DateTime _date) + { + return _date.ToUniversalTime().ToString("o"); + } + + /// <inheritdoc /> + public long SanitizePositionTicks(long? positionTicks) + { + var ticks = positionTicks ?? 0; + ticks = ticks >= 0 ? ticks : 0; + if (_group.PlayingItem != null) + { + var runTimeTicks = _group.PlayingItem.RunTimeTicks ?? 0; + ticks = ticks > runTimeTicks ? runTimeTicks : ticks; + } + + return ticks; + } + + /// <inheritdoc /> public void CreateGroup(SessionInfo session, CancellationToken cancellationToken) { _group.AddSession(session); _syncPlayManager.AddSessionToGroup(session, this); + State = new PausedGroupState(); + _group.PlayingItem = session.FullNowPlayingItem; - _group.IsPaused = session.PlayState.IsPaused; + // 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, BroadcastType.CurrentSession, updateSession, cancellationToken); + 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); } /// <inheritdoc /> @@ -219,21 +213,21 @@ namespace Emby.Server.Implementations.SyncPlay _syncPlayManager.AddSessionToGroup(session, this); var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); - SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); + SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName); - SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); + SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); // Syncing will happen client-side - if (!_group.IsPaused) + if (State.GetGroupState().Equals(GroupState.Playing)) { var playCommand = NewSyncPlayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.CurrentSession, playCommand, cancellationToken); + SendCommand(session, SyncPlayBroadcastType.CurrentSession, playCommand, cancellationToken); } else { var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken); + SendCommand(session, SyncPlayBroadcastType.CurrentSession, pauseCommand, cancellationToken); } } else @@ -244,7 +238,7 @@ namespace Emby.Server.Implementations.SyncPlay StartPositionTicks = _group.PositionTicks }; var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest); - SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken); + SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, update, cancellationToken); } } @@ -255,247 +249,21 @@ namespace Emby.Server.Implementations.SyncPlay _syncPlayManager.RemoveSessionFromGroup(session, this); var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupLeft, _group.PositionTicks); - SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); + SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserLeft, session.UserName); - SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); + SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); } /// <inheritdoc /> - public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken 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. - 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; - } - } - - /// <summary> - /// Handles a play action requested by a session. - /// </summary> - /// <param name="session">The session.</param> - /// <param name="request">The play action.</param> - /// <param name="cancellationToken">The cancellation token.</param> - 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); - } - } - - /// <summary> - /// Handles a pause action requested by a session. - /// </summary> - /// <param name="session">The session.</param> - /// <param name="request">The pause action.</param> - /// <param name="cancellationToken">The cancellation token.</param> - 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); - } - } - - /// <summary> - /// Handles a seek action requested by a session. - /// </summary> - /// <param name="session">The session.</param> - /// <param name="request">The seek action.</param> - /// <param name="cancellationToken">The cancellation token.</param> - 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); - } - - /// <summary> - /// Handles a buffering action requested by a session. - /// </summary> - /// <param name="session">The session.</param> - /// <param name="request">The buffering action.</param> - /// <param name="cancellationToken">The cancellation token.</param> - 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); - } - } - - /// <summary> - /// Handles a buffering-done action requested by a session. - /// </summary> - /// <param name="session">The session.</param> - /// <param name="request">The buffering-done action.</param> - /// <param name="cancellationToken">The cancellation token.</param> - 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); - } - } - - /// <summary> - /// Sanitizes the PositionTicks, considers the current playing item when available. - /// </summary> - /// <param name="positionTicks">The PositionTicks.</param> - /// <value>The sanitized PositionTicks.</value> - private long SanitizePositionTicks(long? positionTicks) - { - var ticks = positionTicks ?? 0; - ticks = ticks >= 0 ? ticks : 0; - if (_group.PlayingItem != null) - { - var runTimeTicks = _group.PlayingItem.RunTimeTicks ?? 0; - ticks = ticks > runTimeTicks ? runTimeTicks : ticks; - } - - return ticks; - } - - /// <summary> - /// Updates ping of a session. - /// </summary> - /// <param name="session">The session.</param> - /// <param name="request">The update.</param> - private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request) - { - // Collected pings are used to account for network latency when unpausing playback - _group.UpdatePing(session, request.Ping ?? GroupInfo.DefaultPing); + _logger.LogInformation("HandleRequest: {0}:{1}.", request.GetType(), State.GetGroupState()); + _ = request.Apply(this, State, session, cancellationToken); + // TODO: do something with returned value } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs index 550939d70..b85f3c149 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 } /// <inheritdoc /> - 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 6035f84e4..bd9670f07 100644 --- a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs +++ b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs @@ -172,7 +172,7 @@ namespace MediaBrowser.Api.SyncPlay /// </summary> /// <param name="request">The request.</param> /// <value>The requested list of groups.</value> - public List<GroupInfoDto> Get(SyncPlayListGroups request) + public List<GroupInfoDto> 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 a1cada25c..cdd24d0b5 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs @@ -31,12 +31,6 @@ namespace MediaBrowser.Controller.SyncPlay public BaseItem PlayingItem { get; set; } /// <summary> - /// Gets or sets a value indicating whether playback is paused. - /// </summary> - /// <value>Playback is paused.</value> - public bool IsPaused { get; set; } - - /// <summary> /// Gets or sets a value indicating whether there are position ticks. /// </summary> /// <value>The position ticks.</value> diff --git a/MediaBrowser.Controller/SyncPlay/IPlaybackGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/IPlaybackGroupRequest.cs new file mode 100644 index 000000000..a6e87a007 --- /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 +{ + /// <summary> + /// Interface IPlaybackGroupRequest. + /// </summary> + public interface IPlaybackGroupRequest + { + /// <summary> + /// Gets the playback request type. + /// </summary> + /// <value>The playback request type.</value> + PlaybackRequestType Type(); + + /// <summary> + /// Applies the request to a group. + /// </summary> + /// <value>The operation completion status.</value> + 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 d869c05bd..5ac2aeb24 100644 --- a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs @@ -56,7 +56,7 @@ namespace MediaBrowser.Controller.SyncPlay /// <param name="session">The session.</param> /// <param name="request">The requested action.</param> /// <param name="cancellationToken">The cancellation token.</param> - void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken); + void HandleRequest(SessionInfo session, IPlaybackGroupRequest request, CancellationToken cancellationToken); /// <summary> /// Gets the info about the group for the clients. diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs index 65770021d..6fa94e2ce 100644 --- a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs @@ -48,7 +48,7 @@ namespace MediaBrowser.Controller.SyncPlay /// <param name="session">The session.</param> /// <param name="request">The request.</param> /// <param name="cancellationToken">The cancellation token.</param> - void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken); + void HandleRequest(SessionInfo session, IPlaybackGroupRequest request, CancellationToken cancellationToken); /// <summary> /// 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 000000000..55c9ee938 --- /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 +{ + /// <summary> + /// Interface ISyncPlayState. + /// </summary> + public interface ISyncPlayState + { + /// <summary> + /// Gets the group state. + /// </summary> + /// <value>The group state.</value> + GroupState GetGroupState(); + + /// <summary> + /// Generic handle. Context's state can change. + /// </summary> + /// <param name="context">The context of the state.</param> + /// <param name="newState">Whether the state has been just set.</param> + /// <param name="request">The play action.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <value>The operation completion status.</value> + bool HandleRequest(ISyncPlayStateContext context, bool newState, IPlaybackGroupRequest request, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a play action requested by a session. Context's state can change. + /// </summary> + /// <param name="context">The context of the state.</param> + /// <param name="newState">Whether the state has been just set.</param> + /// <param name="request">The play action.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <value>The operation completion status.</value> + bool HandleRequest(ISyncPlayStateContext context, bool newState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a pause action requested by a session. Context's state can change. + /// </summary> + /// <param name="context">The context of the state.</param> + /// <param name="newState">Whether the state has been just set.</param> + /// <param name="request">The pause action.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <value>The operation completion status.</value> + bool HandleRequest(ISyncPlayStateContext context, bool newState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a seek action requested by a session. Context's state can change. + /// </summary> + /// <param name="context">The context of the state.</param> + /// <param name="newState">Whether the state has been just set.</param> + /// <param name="request">The seek action.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <value>The operation completion status.</value> + bool HandleRequest(ISyncPlayStateContext context, bool newState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a buffering action requested by a session. Context's state can change. + /// </summary> + /// <param name="context">The context of the state.</param> + /// <param name="newState">Whether the state has been just set.</param> + /// <param name="request">The buffering action.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <value>The operation completion status.</value> + bool HandleRequest(ISyncPlayStateContext context, bool newState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Handles a buffering-done action requested by a session. Context's state can change. + /// </summary> + /// <param name="context">The context of the state.</param> + /// <param name="newState">Whether the state has been just set.</param> + /// <param name="request">The buffering-done action.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <value>The operation completion status.</value> + bool HandleRequest(ISyncPlayStateContext context, bool newState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken); + + /// <summary> + /// Updates ping of a session. Context's state should not change. + /// </summary> + /// <param name="context">The context of the state.</param> + /// <param name="newState">Whether the state has been just set.</param> + /// <param name="request">The buffering-done action.</param> + /// <param name="session">The session.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <value>The operation completion status.</value> + 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 000000000..9bdb1ace6 --- /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 +{ + /// <summary> + /// Interface ISyncPlayStateContext. + /// </summary> + public interface ISyncPlayStateContext + { + /// <summary> + /// Gets the context's group. + /// </summary> + /// <value>The group.</value> + GroupInfo GetGroup(); + + /// <summary> + /// Sets a new state. + /// </summary> + /// <param name="state">The new state.</param> + void SetState(ISyncPlayState state); + + /// <summary> + /// Sends a GroupUpdate message to the interested sessions. + /// </summary> + /// <param name="from">The current session.</param> + /// <param name="type">The filtering type.</param> + /// <param name="message">The message to send.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <value>The task.</value> + Task SendGroupUpdate<T>(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken); + + /// <summary> + /// Sends a playback command to the interested sessions. + /// </summary> + /// <param name="from">The current session.</param> + /// <param name="type">The filtering type.</param> + /// <param name="message">The message to send.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <value>The task.</value> + Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken cancellationToken); + + /// <summary> + /// Builds a new playback command with some default values. + /// </summary> + /// <param name="type">The command type.</param> + /// <value>The SendCommand.</value> + SendCommand NewSyncPlayCommand(SendCommandType type); + + /// <summary> + /// Builds a new group update message. + /// </summary> + /// <param name="type">The update type.</param> + /// <param name="data">The data to send.</param> + /// <value>The GroupUpdate.</value> + GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data); + + /// <summary> + /// Converts DateTime to UTC string. + /// </summary> + /// <param name="date">The date to convert.</param> + /// <value>The UTC string.</value> + string DateToUTCString(DateTime date); + + /// <summary> + /// Sanitizes the PositionTicks, considers the current playing item when available. + /// </summary> + /// <param name="positionTicks">The PositionTicks.</param> + /// <value>The sanitized PositionTicks.</value> + 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 000000000..21dae8e4e --- /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 +{ + /// <summary> + /// Class BufferingGroupRequest. + /// </summary> + public class BufferGroupRequest : IPlaybackGroupRequest + { + /// <summary> + /// Gets or sets when the request has been made by the client. + /// </summary> + /// <value>The date of the request.</value> + public DateTime When { get; set; } + + /// <summary> + /// Gets or sets the position ticks. + /// </summary> + /// <value>The position ticks.</value> + public long PositionTicks { get; set; } + + /// <summary> + /// Gets or sets the playing item id. + /// </summary> + /// <value>The playing item id.</value> + public Guid PlayingItemId { get; set; } + + /// <inheritdoc /> + public PlaybackRequestType Type() + { + return PlaybackRequestType.Buffer; + } + + /// <inheritdoc /> + 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 000000000..21a46add8 --- /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 +{ + /// <summary> + /// Class PauseGroupRequest. + /// </summary> + public class PauseGroupRequest : IPlaybackGroupRequest + { + /// <inheritdoc /> + public PlaybackRequestType Type() + { + return PlaybackRequestType.Pause; + } + + /// <inheritdoc /> + 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 000000000..2f78edfc5 --- /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 +{ + /// <summary> + /// Class UpdatePingGroupRequest. + /// </summary> + public class PingGroupRequest : IPlaybackGroupRequest + { + /// <summary> + /// Gets or sets the ping time. + /// </summary> + /// <value>The ping time.</value> + public long Ping { get; set; } + + /// <inheritdoc /> + public PlaybackRequestType Type() + { + return PlaybackRequestType.Ping; + } + + /// <inheritdoc /> + 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 000000000..942229a77 --- /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 +{ + /// <summary> + /// Class PlayGroupRequest. + /// </summary> + public class PlayGroupRequest : IPlaybackGroupRequest + { + /// <inheritdoc /> + public PlaybackRequestType Type() + { + return PlaybackRequestType.Play; + } + + /// <inheritdoc /> + 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 000000000..ee88ddddb --- /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 +{ + /// <summary> + /// Class BufferingDoneGroupRequest. + /// </summary> + public class ReadyGroupRequest : IPlaybackGroupRequest + { + /// <summary> + /// Gets or sets when the request has been made by the client. + /// </summary> + /// <value>The date of the request.</value> + public DateTime When { get; set; } + + /// <summary> + /// Gets or sets the position ticks. + /// </summary> + /// <value>The position ticks.</value> + public long PositionTicks { get; set; } + + /// <summary> + /// Gets or sets the playing item id. + /// </summary> + /// <value>The playing item id.</value> + public Guid PlayingItemId { get; set; } + + /// <inheritdoc /> + public PlaybackRequestType Type() + { + return PlaybackRequestType.Ready; + } + + /// <inheritdoc /> + 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 000000000..bb5e7a343 --- /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 +{ + /// <summary> + /// Class SeekGroupRequest. + /// </summary> + public class SeekGroupRequest : IPlaybackGroupRequest + { + /// <summary> + /// Gets or sets the position ticks. + /// </summary> + /// <value>The position ticks.</value> + public long PositionTicks { get; set; } + + /// <inheritdoc /> + public PlaybackRequestType Type() + { + return PlaybackRequestType.Seek; + } + + /// <inheritdoc /> + 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 000000000..0b72d1668 --- /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 +{ + /// <summary> + /// Class SyncPlayAbstractState. + /// </summary> + /// <remarks> + /// Class is not thread-safe, external locking is required when accessing methods. + /// </remarks> + public abstract class SyncPlayAbstractState : ISyncPlayState + { + /// <inheritdoc /> + public abstract GroupState GetGroupState(); + + /// <inheritdoc /> + public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, IPlaybackGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + return true; + } + + /// <inheritdoc /> + public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + return true; + } + + /// <inheritdoc /> + public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + return true; + } + + /// <inheritdoc /> + public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + return true; + } + + /// <inheritdoc /> + public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + return true; + } + + /// <inheritdoc /> + public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken) + { + return true; + } + + /// <inheritdoc /> + 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 000000000..871634d55 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/GroupState.cs @@ -0,0 +1,25 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// <summary> + /// Enum GroupState. + /// </summary> + public enum GroupState + { + /// <summary> + /// The group is in idle state. No media is playing. + /// </summary> + Idle, + /// <summary> + /// The group is in wating state. Playback is paused. Will start playing when users are ready. + /// </summary> + Waiting, + /// <summary> + /// The group is in paused state. Playback is paused. Will resume on play command. + /// </summary> + Paused, + /// <summary> + /// The group is in playing state. Playback is advancing. + /// </summary> + Playing + } +} diff --git a/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs b/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs deleted file mode 100644 index 9de23194e..000000000 --- a/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; - -namespace MediaBrowser.Model.SyncPlay -{ - /// <summary> - /// Class PlaybackRequest. - /// </summary> - public class PlaybackRequest - { - /// <summary> - /// Gets or sets the request type. - /// </summary> - /// <value>The request type.</value> - public PlaybackRequestType Type { get; set; } - - /// <summary> - /// Gets or sets when the request has been made by the client. - /// </summary> - /// <value>The date of the request.</value> - public DateTime? When { get; set; } - - /// <summary> - /// Gets or sets the position ticks. - /// </summary> - /// <value>The position ticks.</value> - public long? PositionTicks { get; set; } - - /// <summary> - /// Gets or sets the ping time. - /// </summary> - /// <value>The ping time.</value> - public long? Ping { get; set; } - } -} diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayBroadcastType.cs b/MediaBrowser.Model/SyncPlay/SyncPlayBroadcastType.cs new file mode 100644 index 000000000..29dbb11b3 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/SyncPlayBroadcastType.cs @@ -0,0 +1,28 @@ +namespace MediaBrowser.Model.SyncPlay +{ + /// <summary> + /// Used to filter the sessions of a group. + /// </summary> + public enum SyncPlayBroadcastType + { + /// <summary> + /// All sessions will receive the message. + /// </summary> + AllGroup = 0, + + /// <summary> + /// Only the specified session will receive the message. + /// </summary> + CurrentSession = 1, + + /// <summary> + /// All sessions, except the current one, will receive the message. + /// </summary> + AllExceptCurrentSession = 2, + + /// <summary> + /// Only sessions that are not buffering will receive the message. + /// </summary> + AllReady = 3 + } +} |
