diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 607b322f2e..9a94d05eb0 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1182,18 +1182,16 @@ namespace Emby.Server.Implementations.Session
}
///
- public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken)
+ public async Task SendSyncPlayCommand(SessionInfo session, SendCommand command, CancellationToken cancellationToken)
{
CheckDisposed();
- var session = GetSessionToRemoteControl(sessionId);
await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).ConfigureAwait(false);
}
///
- public async Task SendSyncPlayGroupUpdate(string sessionId, GroupUpdate command, CancellationToken cancellationToken)
+ public async Task SendSyncPlayGroupUpdate(SessionInfo session, GroupUpdate command, CancellationToken cancellationToken)
{
CheckDisposed();
- var session = GetSessionToRemoteControl(sessionId);
await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).ConfigureAwait(false);
}
diff --git a/Emby.Server.Implementations/SyncPlay/GroupController.cs b/Emby.Server.Implementations/SyncPlay/GroupController.cs
new file mode 100644
index 0000000000..ee2e9eb8f1
--- /dev/null
+++ b/Emby.Server.Implementations/SyncPlay/GroupController.cs
@@ -0,0 +1,681 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.SyncPlay;
+using MediaBrowser.Model.SyncPlay;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.SyncPlay
+{
+ ///
+ /// Class SyncPlayGroupController.
+ ///
+ ///
+ /// Class is not thread-safe, external locking is required when accessing methods.
+ ///
+ public class SyncPlayGroupController : ISyncPlayGroupController, ISyncPlayStateContext
+ {
+ ///
+ /// Gets the default ping value used for sessions.
+ ///
+ public long DefaultPing { get; } = 500;
+
+ ///
+ /// The logger.
+ ///
+ private readonly ILogger _logger;
+
+ ///
+ /// The user manager.
+ ///
+ private readonly IUserManager _userManager;
+
+ ///
+ /// The session manager.
+ ///
+ private readonly ISessionManager _sessionManager;
+
+ ///
+ /// The library manager.
+ ///
+ private readonly ILibraryManager _libraryManager;
+
+ ///
+ /// The SyncPlay manager.
+ ///
+ private readonly ISyncPlayManager _syncPlayManager;
+
+ ///
+ /// Internal group state.
+ ///
+ /// The group's state.
+ private ISyncPlayState State;
+
+ ///
+ /// Gets the group identifier.
+ ///
+ /// The group identifier.
+ public Guid GroupId { get; } = Guid.NewGuid();
+
+ ///
+ /// Gets the group name.
+ ///
+ /// The group name.
+ public string GroupName { get; private set; }
+
+ ///
+ /// Gets the group identifier.
+ ///
+ /// The group identifier.
+ public PlayQueueManager PlayQueue { get; } = new PlayQueueManager();
+
+ ///
+ /// Gets or sets the runtime ticks of current playing item.
+ ///
+ /// The runtime ticks of current playing item.
+ public long RunTimeTicks { get; private set; }
+
+ ///
+ /// Gets or sets the position ticks.
+ ///
+ /// The position ticks.
+ public long PositionTicks { get; set; }
+
+ ///
+ /// Gets or sets the last activity.
+ ///
+ /// The last activity.
+ public DateTime LastActivity { get; set; }
+
+ ///
+ /// Gets the participants.
+ ///
+ /// The participants, or members of the group.
+ public Dictionary Participants { get; } =
+ new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger.
+ /// The user manager.
+ /// The session manager.
+ /// The library manager.
+ /// The SyncPlay manager.
+ public SyncPlayGroupController(
+ ILogger logger,
+ IUserManager userManager,
+ ISessionManager sessionManager,
+ ILibraryManager libraryManager,
+ ISyncPlayManager syncPlayManager)
+ {
+ _logger = logger;
+ _userManager = userManager;
+ _sessionManager = sessionManager;
+ _libraryManager = libraryManager;
+ _syncPlayManager = syncPlayManager;
+
+ State = new IdleGroupState(_logger);
+ }
+
+ ///
+ /// Checks if a session is in this group.
+ ///
+ /// The session id to check.
+ /// true if the session is in this group; false otherwise.
+ private bool ContainsSession(string sessionId)
+ {
+ return Participants.ContainsKey(sessionId);
+ }
+
+ ///
+ /// Adds the session to the group.
+ ///
+ /// The session.
+ private void AddSession(SessionInfo session)
+ {
+ Participants.TryAdd(
+ session.Id,
+ new GroupMember
+ {
+ Session = session,
+ Ping = DefaultPing,
+ IsBuffering = false
+ });
+ }
+
+ ///
+ /// Removes the session from the group.
+ ///
+ /// The session.
+ private void RemoveSession(SessionInfo session)
+ {
+ Participants.Remove(session.Id);
+ }
+
+ ///
+ /// Filters sessions of this group.
+ ///
+ /// The current session.
+ /// The filtering type.
+ /// The array of sessions matching the filter.
+ private SessionInfo[] FilterSessions(SessionInfo from, SyncPlayBroadcastType type)
+ {
+ switch (type)
+ {
+ case SyncPlayBroadcastType.CurrentSession:
+ return new SessionInfo[] { from };
+ case SyncPlayBroadcastType.AllGroup:
+ return Participants.Values.Select(
+ session => session.Session).ToArray();
+ case SyncPlayBroadcastType.AllExceptCurrentSession:
+ return Participants.Values.Select(
+ session => session.Session).Where(
+ session => !session.Id.Equals(from.Id)).ToArray();
+ case SyncPlayBroadcastType.AllReady:
+ return Participants.Values.Where(
+ session => !session.IsBuffering).Select(
+ session => session.Session).ToArray();
+ default:
+ return Array.Empty();
+ }
+ }
+
+ private bool HasAccessToItem(User user, BaseItem item)
+ {
+ var collections = _libraryManager.GetCollectionFolders(item)
+ .Select(folder => folder.Id.ToString("N", CultureInfo.InvariantCulture));
+ return collections.Intersect(user.GetPreference(PreferenceKind.EnabledFolders)).Any();
+ }
+
+ private bool HasAccessToQueue(User user, Guid[] queue)
+ {
+ if (queue == null || queue.Length == 0)
+ {
+ return true;
+ }
+
+ var items = queue.ToList()
+ .Select(item => _libraryManager.GetItemById(item));
+
+ // Find the highest rating value, which becomes the required minimum for the user
+ var MinParentalRatingAccessRequired = items
+ .Select(item => item.InheritedParentalRatingValue)
+ .Min();
+
+ // Check ParentalRating access, user must have the minimum required access level
+ var hasParentalRatingAccess = !user.MaxParentalAgeRating.HasValue
+ || MinParentalRatingAccessRequired <= user.MaxParentalAgeRating;
+
+ // Check that user has access to all required folders
+ if (!user.HasPermission(PermissionKind.EnableAllFolders) && hasParentalRatingAccess)
+ {
+ // Get list of items that are not accessible
+ var blockedItems = items.Where(item => !HasAccessToItem(user, item));
+
+ // We need the user to be able to access all items
+ return !blockedItems.Any();
+ }
+
+ return hasParentalRatingAccess;
+ }
+
+ private bool AllUsersHaveAccessToQueue(Guid[] queue)
+ {
+ if (queue == null || queue.Length == 0)
+ {
+ return true;
+ }
+
+ // Get list of users
+ var users = Participants.Values
+ .Select(participant => _userManager.GetUserById(participant.Session.UserId));
+
+ // Find problematic users
+ var usersWithNoAccess = users.Where(user => !HasAccessToQueue(user, queue));
+
+ // All users must be able to access the queue
+ return !usersWithNoAccess.Any();
+ }
+
+ ///
+ public bool IsGroupEmpty() => Participants.Count == 0;
+
+ ///
+ public void CreateGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken)
+ {
+ GroupName = request.GroupName;
+ AddSession(session);
+ _syncPlayManager.AddSessionToGroup(session, this);
+
+ var sessionIsPlayingAnItem = session.FullNowPlayingItem != null;
+
+ RestartCurrentItem();
+
+ if (sessionIsPlayingAnItem)
+ {
+ var playlist = session.NowPlayingQueue.Select(item => item.Id).ToArray();
+ PlayQueue.SetPlaylist(playlist);
+ PlayQueue.SetPlayingItemById(session.FullNowPlayingItem.Id);
+ RunTimeTicks = session.FullNowPlayingItem.RunTimeTicks ?? 0;
+ PositionTicks = session.PlayState.PositionTicks ?? 0;
+
+ // Mantain playstate
+ var waitingState = new WaitingGroupState(_logger);
+ waitingState.ResumePlaying = !session.PlayState.IsPaused;
+ SetState(waitingState);
+ }
+
+ var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, GetInfo());
+ SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
+
+ State.SessionJoined(this, State.GetGroupState(), session, cancellationToken);
+
+ _logger.LogInformation("InitGroup: {0} created group {1}.", session.Id.ToString(), GroupId.ToString());
+ }
+
+ ///
+ public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
+ {
+ AddSession(session);
+ _syncPlayManager.AddSessionToGroup(session, this);
+
+ var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, GetInfo());
+ SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
+
+ var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
+ SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
+
+ State.SessionJoined(this, State.GetGroupState(), session, cancellationToken);
+
+ _logger.LogInformation("SessionJoin: {0} joined group {1}.", session.Id.ToString(), GroupId.ToString());
+ }
+
+ ///
+ public void SessionRestore(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
+ {
+ var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, GetInfo());
+ SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
+
+ var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
+ SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
+
+ State.SessionJoined(this, State.GetGroupState(), session, cancellationToken);
+
+ _logger.LogInformation("SessionRestore: {0} re-joined group {1}.", session.Id.ToString(), GroupId.ToString());
+ }
+
+ ///
+ public void SessionLeave(SessionInfo session, CancellationToken cancellationToken)
+ {
+ State.SessionLeaving(this, State.GetGroupState(), session, cancellationToken);
+
+ RemoveSession(session);
+ _syncPlayManager.RemoveSessionFromGroup(session, this);
+
+ var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupLeft, GroupId.ToString());
+ SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
+
+ var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserLeft, session.UserName);
+ SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
+
+ _logger.LogInformation("SessionLeave: {0} left group {1}.", session.Id.ToString(), GroupId.ToString());
+ }
+
+ ///
+ 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} requested {1}, group {2} in {3} state.",
+ session.Id.ToString(), request.GetRequestType(), GroupId.ToString(), State.GetGroupState());
+ request.Apply(this, State, session, cancellationToken);
+ }
+
+ ///
+ public GroupInfoDto GetInfo()
+ {
+ return new GroupInfoDto()
+ {
+ GroupId = GroupId.ToString(),
+ GroupName = GroupName,
+ State = State.GetGroupState(),
+ Participants = Participants.Values.Select(session => session.Session.UserName).Distinct().ToList(),
+ LastUpdatedAt = DateToUTCString(DateTime.UtcNow)
+ };
+ }
+
+ ///
+ public bool HasAccessToPlayQueue(User user)
+ {
+ var items = PlayQueue.GetPlaylist().Select(item => item.ItemId).ToArray();
+ return HasAccessToQueue(user, items);
+ }
+
+ ///
+ public void SetIgnoreGroupWait(SessionInfo session, bool ignoreGroupWait)
+ {
+ if (!ContainsSession(session.Id))
+ {
+ return;
+ }
+
+ Participants[session.Id].IgnoreGroupWait = ignoreGroupWait;
+ }
+
+ ///
+ public void SetState(ISyncPlayState state)
+ {
+ _logger.LogInformation("SetState: {0} switching from {1} to {2}.", GroupId.ToString(), State.GetGroupState(), state.GetGroupState());
+ this.State = state;
+ }
+
+ ///
+ public Task SendGroupUpdate(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate message, CancellationToken cancellationToken)
+ {
+ IEnumerable GetTasks()
+ {
+ foreach (var session in FilterSessions(from, type))
+ {
+ yield return _sessionManager.SendSyncPlayGroupUpdate(session, message, cancellationToken);
+ }
+ }
+
+ return Task.WhenAll(GetTasks());
+ }
+
+ ///
+ public Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken cancellationToken)
+ {
+ IEnumerable GetTasks()
+ {
+ foreach (var session in FilterSessions(from, type))
+ {
+ yield return _sessionManager.SendSyncPlayCommand(session, message, cancellationToken);
+ }
+ }
+
+ return Task.WhenAll(GetTasks());
+ }
+
+ ///
+ public SendCommand NewSyncPlayCommand(SendCommandType type)
+ {
+ return new SendCommand()
+ {
+ GroupId = GroupId.ToString(),
+ PlaylistItemId = PlayQueue.GetPlayingItemPlaylistId(),
+ PositionTicks = PositionTicks,
+ Command = type,
+ When = DateToUTCString(LastActivity),
+ EmittedAt = DateToUTCString(DateTime.UtcNow)
+ };
+ }
+
+ ///
+ public GroupUpdate NewSyncPlayGroupUpdate(GroupUpdateType type, T data)
+ {
+ return new GroupUpdate()
+ {
+ GroupId = GroupId.ToString(),
+ Type = type,
+ Data = data
+ };
+ }
+
+ ///
+ public string DateToUTCString(DateTime dateTime)
+ {
+ return dateTime.ToUniversalTime().ToString("o");
+ }
+
+ ///
+ public long SanitizePositionTicks(long? positionTicks)
+ {
+ var ticks = positionTicks ?? 0;
+ ticks = ticks >= 0 ? ticks : 0;
+ ticks = ticks > RunTimeTicks ? RunTimeTicks : ticks;
+ return ticks;
+ }
+
+ ///
+ public void UpdatePing(SessionInfo session, long ping)
+ {
+ if (Participants.TryGetValue(session.Id, out GroupMember value))
+ {
+ value.Ping = ping;
+ }
+ }
+
+ ///
+ public long GetHighestPing()
+ {
+ long max = long.MinValue;
+ foreach (var session in Participants.Values)
+ {
+ max = Math.Max(max, session.Ping);
+ }
+
+ return max;
+ }
+
+ ///
+ public void SetBuffering(SessionInfo session, bool isBuffering)
+ {
+ if (Participants.TryGetValue(session.Id, out GroupMember value))
+ {
+ value.IsBuffering = isBuffering;
+ }
+ }
+
+ ///
+ public void SetAllBuffering(bool isBuffering)
+ {
+ foreach (var session in Participants.Values)
+ {
+ session.IsBuffering = isBuffering;
+ }
+ }
+
+ ///
+ public bool IsBuffering()
+ {
+ foreach (var session in Participants.Values)
+ {
+ if (session.IsBuffering && !session.IgnoreGroupWait)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ public bool SetPlayQueue(Guid[] playQueue, int playingItemPosition, long startPositionTicks)
+ {
+ // Ignore on empty queue or invalid item position
+ if (playQueue.Length < 1 || playingItemPosition >= playQueue.Length || playingItemPosition < 0)
+ {
+ return false;
+ }
+
+ // Check is participants can access the new playing queue
+ if (!AllUsersHaveAccessToQueue(playQueue))
+ {
+ return false;
+ }
+
+ PlayQueue.SetPlaylist(playQueue);
+ PlayQueue.SetPlayingItemByIndex(playingItemPosition);
+ var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId());
+ RunTimeTicks = item.RunTimeTicks ?? 0;
+ PositionTicks = startPositionTicks;
+ LastActivity = DateTime.UtcNow;
+
+ return true;
+ }
+
+ ///
+ public bool SetPlayingItem(string playlistItemId)
+ {
+ var itemFound = PlayQueue.SetPlayingItemByPlaylistId(playlistItemId);
+
+ if (itemFound)
+ {
+ var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId());
+ RunTimeTicks = item.RunTimeTicks ?? 0;
+ }
+ else
+ {
+ RunTimeTicks = 0;
+ }
+
+ RestartCurrentItem();
+
+ return itemFound;
+ }
+
+ ///
+ public bool RemoveFromPlayQueue(string[] playlistItemIds)
+ {
+ var playingItemRemoved = PlayQueue.RemoveFromPlaylist(playlistItemIds);
+ if (playingItemRemoved)
+ {
+ var itemId = PlayQueue.GetPlayingItemId();
+ if (!itemId.Equals(Guid.Empty))
+ {
+ var item = _libraryManager.GetItemById(itemId);
+ RunTimeTicks = item.RunTimeTicks ?? 0;
+ }
+ else
+ {
+ RunTimeTicks = 0;
+ }
+
+ RestartCurrentItem();
+ }
+
+ return playingItemRemoved;
+ }
+
+ ///
+ public bool MoveItemInPlayQueue(string playlistItemId, int newIndex)
+ {
+ return PlayQueue.MovePlaylistItem(playlistItemId, newIndex);
+ }
+
+ ///
+ public bool AddToPlayQueue(Guid[] newItems, string mode)
+ {
+ // Ignore on empty list
+ if (newItems.Length < 1)
+ {
+ return false;
+ }
+
+ // Check is participants can access the new playing queue
+ if (!AllUsersHaveAccessToQueue(newItems))
+ {
+ return false;
+ }
+
+ if (mode.Equals("next"))
+ {
+ PlayQueue.QueueNext(newItems);
+ }
+ else
+ {
+ PlayQueue.Queue(newItems);
+ }
+
+ return true;
+ }
+
+ ///
+ public void RestartCurrentItem()
+ {
+ PositionTicks = 0;
+ LastActivity = DateTime.UtcNow;
+ }
+
+ ///
+ public bool NextItemInQueue()
+ {
+ var update = PlayQueue.Next();
+ if (update)
+ {
+ var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId());
+ RunTimeTicks = item.RunTimeTicks ?? 0;
+ RestartCurrentItem();
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ ///
+ public bool PreviousItemInQueue()
+ {
+ var update = PlayQueue.Previous();
+ if (update)
+ {
+ var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId());
+ RunTimeTicks = item.RunTimeTicks ?? 0;
+ RestartCurrentItem();
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ ///
+ public void SetRepeatMode(string mode) {
+ PlayQueue.SetRepeatMode(mode);
+ }
+
+ ///
+ public void SetShuffleMode(string mode) {
+ PlayQueue.SetShuffleMode(mode);
+ }
+
+ ///
+ public PlayQueueUpdate GetPlayQueueUpdate(PlayQueueUpdateReason reason)
+ {
+ var startPositionTicks = PositionTicks;
+
+ if (State.GetGroupState().Equals(GroupState.Playing))
+ {
+ var currentTime = DateTime.UtcNow;
+ var elapsedTime = currentTime - LastActivity;
+ // Event may happen during the delay added to account for latency
+ startPositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
+ }
+
+ return new PlayQueueUpdate()
+ {
+ Reason = reason,
+ LastUpdate = DateToUTCString(PlayQueue.LastChange),
+ Playlist = PlayQueue.GetPlaylist(),
+ PlayingItemIndex = PlayQueue.PlayingItemIndex,
+ StartPositionTicks = startPositionTicks,
+ ShuffleMode = PlayQueue.ShuffleMode,
+ RepeatMode = PlayQueue.RepeatMode
+ };
+ }
+
+ }
+}
diff --git a/Emby.Server.Implementations/SyncPlay/GroupStates/AbstractGroupState.cs b/Emby.Server.Implementations/SyncPlay/GroupStates/AbstractGroupState.cs
new file mode 100644
index 0000000000..1f0cb42870
--- /dev/null
+++ b/Emby.Server.Implementations/SyncPlay/GroupStates/AbstractGroupState.cs
@@ -0,0 +1,218 @@
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ ///
+ /// Class AbstractGroupState.
+ ///
+ ///
+ /// Class is not thread-safe, external locking is required when accessing methods.
+ ///
+ public abstract class AbstractGroupState : ISyncPlayState
+ {
+ ///
+ /// The logger.
+ ///
+ protected readonly ILogger _logger;
+
+ ///
+ /// Default constructor.
+ ///
+ public AbstractGroupState(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ ///
+ /// Sends a group state update to all group.
+ ///
+ /// The context of the state.
+ /// The reason of the state change.
+ /// The session.
+ /// The cancellation token.
+ protected void SendGroupStateUpdate(ISyncPlayStateContext context, IPlaybackGroupRequest reason, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Notify relevant state change event
+ var stateUpdate = new GroupStateUpdate()
+ {
+ State = GetGroupState(),
+ Reason = reason.GetRequestType()
+ };
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.StateUpdate, stateUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+ }
+
+ ///
+ public abstract GroupState GetGroupState();
+
+ ///
+ public abstract void SessionJoined(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ ///
+ public abstract void SessionLeaving(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, IPlaybackGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, SetPlaylistItemGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, RemoveFromPlaylistGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ var playingItemRemoved = context.RemoveFromPlayQueue(request.PlaylistItemIds);
+
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.RemoveItems);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+
+ if (playingItemRemoved)
+ {
+ var PlayingItemIndex = context.PlayQueue.PlayingItemIndex;
+ if (context.PlayQueue.PlayingItemIndex == -1)
+ {
+ _logger.LogDebug("HandleRequest: {0} in group {1}, play queue is empty.", request.GetRequestType(), context.GroupId.ToString());
+
+ ISyncPlayState idleState = new IdleGroupState(_logger);
+ context.SetState(idleState);
+ var stopRequest = new StopGroupRequest();
+ idleState.HandleRequest(context, GetGroupState(), stopRequest, session, cancellationToken);
+ }
+ }
+ }
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, MovePlaylistItemGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ var result = context.MoveItemInPlayQueue(request.PlaylistItemId, request.NewIndex);
+
+ if (!result)
+ {
+ _logger.LogError("HandleRequest: {0} in group {1}, unable to move item in play queue.", request.GetRequestType(), context.GroupId.ToString());
+ return;
+ }
+
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.MoveItem);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+ }
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, QueueGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ var result = context.AddToPlayQueue(request.ItemIds, request.Mode);
+
+ if (!result)
+ {
+ _logger.LogError("HandleRequest: {0} in group {1}, unable to add items to play queue.", request.GetRequestType(), context.GroupId.ToString());
+ return;
+ }
+
+ var reason = request.Mode.Equals("next") ? PlayQueueUpdateReason.QueueNext : PlayQueueUpdateReason.Queue;
+ var playQueueUpdate = context.GetPlayQueueUpdate(reason);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+ }
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, UnpauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, StopGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, NextTrackGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PreviousTrackGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, SetRepeatModeGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ context.SetRepeatMode(request.Mode);
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.RepeatMode);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+ }
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, SetShuffleModeGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ context.SetShuffleMode(request.Mode);
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.ShuffleMode);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+ }
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PingGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Collected pings are used to account for network latency when unpausing playback
+ context.UpdatePing(session, request.Ping);
+ }
+
+ ///
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, IgnoreWaitGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ context.SetIgnoreGroupWait(session, request.IgnoreWait);
+ }
+
+ private void UnhandledRequest(IPlaybackGroupRequest request)
+ {
+ _logger.LogWarning("HandleRequest: unhandled {0} request for {1} state.", request.GetRequestType(), this.GetGroupState());
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/SyncPlay/GroupStates/IdleGroupState.cs b/Emby.Server.Implementations/SyncPlay/GroupStates/IdleGroupState.cs
new file mode 100644
index 0000000000..d6b981c584
--- /dev/null
+++ b/Emby.Server.Implementations/SyncPlay/GroupStates/IdleGroupState.cs
@@ -0,0 +1,121 @@
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ ///
+ /// Class IdleGroupState.
+ ///
+ ///
+ /// Class is not thread-safe, external locking is required when accessing methods.
+ ///
+ public class IdleGroupState : AbstractGroupState
+ {
+ ///
+ /// Default constructor.
+ ///
+ public IdleGroupState(ILogger logger) : base(logger)
+ {
+ // Do nothing
+ }
+
+ ///
+ public override GroupState GetGroupState()
+ {
+ return GroupState.Idle;
+ }
+
+ ///
+ public override void SessionJoined(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, GetGroupState(), session, cancellationToken);
+ }
+
+ ///
+ public override void SessionLeaving(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Do nothing
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, UnpauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, prevState, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, StopGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, prevState, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, prevState, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, prevState, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, prevState, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, NextTrackGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PreviousTrackGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
+
+ private void SendStopCommand(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ var command = context.NewSyncPlayCommand(SendCommandType.Stop);
+ if (!prevState.Equals(GetGroupState()))
+ {
+ context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken);
+ }
+ else
+ {
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/SyncPlay/GroupStates/PausedGroupState.cs b/Emby.Server.Implementations/SyncPlay/GroupStates/PausedGroupState.cs
index d3bf24f747..39c0511d9f 100644
--- a/Emby.Server.Implementations/SyncPlay/GroupStates/PausedGroupState.cs
+++ b/Emby.Server.Implementations/SyncPlay/GroupStates/PausedGroupState.cs
@@ -1,11 +1,8 @@
-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;
+using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.SyncPlay
{
@@ -15,8 +12,16 @@ namespace MediaBrowser.Controller.SyncPlay
///
/// Class is not thread-safe, external locking is required when accessing methods.
///
- public class PausedGroupState : SyncPlayAbstractState
+ public class PausedGroupState : AbstractGroupState
{
+ ///
+ /// Default constructor.
+ ///
+ public PausedGroupState(ILogger logger) : base(logger)
+ {
+ // Do nothing
+ }
+
///
public override GroupState GetGroupState()
{
@@ -24,31 +29,56 @@ namespace MediaBrowser.Controller.SyncPlay
}
///
- public override bool HandleRequest(ISyncPlayStateContext context, bool newState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ public override void SessionJoined(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken)
{
- // Change state
- var playingState = new PlayingGroupState();
- context.SetState(playingState);
- return playingState.HandleRequest(context, true, request, session, cancellationToken);
+ // Wait for session to be ready
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.SessionJoined(context, GetGroupState(), session, cancellationToken);
}
///
- public override bool HandleRequest(ISyncPlayStateContext context, bool newState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ public override void SessionLeaving(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken)
{
- if (newState)
- {
- GroupInfo group = context.GetGroup();
+ // Do nothing
+ }
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, UnpauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state
+ var playingState = new PlayingGroupState(_logger);
+ context.SetState(playingState);
+ playingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ if (!prevState.Equals(GetGroupState()))
+ {
// Pause group and compute the media playback position
var currentTime = DateTime.UtcNow;
- var elapsedTime = currentTime - group.LastActivity;
- group.LastActivity = currentTime;
+ var elapsedTime = currentTime - context.LastActivity;
+ context.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;
+ context.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
var command = context.NewSyncPlayCommand(SendCommandType.Pause);
context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken);
+
+ // Notify relevant state change event
+ SendGroupStateUpdate(context, request, session, cancellationToken);
}
else
{
@@ -56,116 +86,71 @@ namespace MediaBrowser.Controller.SyncPlay
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)
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, StopGroupRequest 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;
+ // Change state
+ var idleState = new IdleGroupState(_logger);
+ context.SetState(idleState);
+ idleState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
}
///
- public override bool HandleRequest(ISyncPlayStateContext context, bool newState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
{
- GroupInfo group = context.GetGroup();
+ // Change state
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
- if (newState)
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ if (prevState.Equals(GetGroupState()))
{
- // 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);
}
+ else if (prevState.Equals(GroupState.Waiting))
+ {
+ // Sending current state to all clients
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken);
- return true;
+ // Notify relevant state change event
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+ }
}
///
- public override bool HandleRequest(ISyncPlayStateContext context, bool newState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, NextTrackGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
{
- GroupInfo group = context.GetGroup();
+ // Change state
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
- 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;
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PreviousTrackGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
}
}
}
diff --git a/Emby.Server.Implementations/SyncPlay/GroupStates/PlayingGroupState.cs b/Emby.Server.Implementations/SyncPlay/GroupStates/PlayingGroupState.cs
index 42c7779c19..e2909ff91c 100644
--- a/Emby.Server.Implementations/SyncPlay/GroupStates/PlayingGroupState.cs
+++ b/Emby.Server.Implementations/SyncPlay/GroupStates/PlayingGroupState.cs
@@ -1,11 +1,8 @@
-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;
+using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.SyncPlay
{
@@ -15,8 +12,21 @@ namespace MediaBrowser.Controller.SyncPlay
///
/// Class is not thread-safe, external locking is required when accessing methods.
///
- public class PlayingGroupState : SyncPlayAbstractState
+ public class PlayingGroupState : AbstractGroupState
{
+ ///
+ /// Ignore requests for buffering.
+ ///
+ public bool IgnoreBuffering { get; set; }
+
+ ///
+ /// Default constructor.
+ ///
+ public PlayingGroupState(ILogger logger) : base(logger)
+ {
+ // Do nothing
+ }
+
///
public override GroupState GetGroupState()
{
@@ -24,71 +34,132 @@ namespace MediaBrowser.Controller.SyncPlay
}
///
- public override bool HandleRequest(ISyncPlayStateContext context, bool newState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ public override void SessionJoined(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken)
{
- GroupInfo group = context.GetGroup();
+ // Wait for session to be ready
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.SessionJoined(context, GetGroupState(), session, cancellationToken);
+ }
- if (newState)
+ ///
+ public override void SessionLeaving(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Do nothing
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, UnpauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ if (!prevState.Equals(GetGroupState()))
{
// Pick a suitable time that accounts for latency
- var delay = Math.Max(group.GetHighestPing() * 2, group.DefaultPing);
+ var delayMillis = Math.Max(context.GetHighestPing() * 2, context.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
+ context.LastActivity = DateTime.UtcNow.AddMilliseconds(
+ delayMillis
);
- var command = context.NewSyncPlayCommand(SendCommandType.Play);
+ var command = context.NewSyncPlayCommand(SendCommandType.Unpause);
context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken);
+
+ // Notify relevant state change event
+ SendGroupStateUpdate(context, request, session, cancellationToken);
}
else
{
// Client got lost, sending current state
- var command = context.NewSyncPlayCommand(SendCommandType.Play);
+ var command = context.NewSyncPlayCommand(SendCommandType.Unpause);
context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
}
-
- return true;
}
///
- public override bool HandleRequest(ISyncPlayStateContext context, bool newState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
{
// Change state
- var pausedState = new PausedGroupState();
+ var pausedState = new PausedGroupState(_logger);
context.SetState(pausedState);
- return pausedState.HandleRequest(context, true, request, session, cancellationToken);
+ pausedState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
}
///
- public override bool HandleRequest(ISyncPlayStateContext context, bool newState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, StopGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
{
// Change state
- var pausedState = new PausedGroupState();
- context.SetState(pausedState);
- return pausedState.HandleRequest(context, true, request, session, cancellationToken);
+ var idleState = new IdleGroupState(_logger);
+ context.SetState(idleState);
+ idleState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
}
///
- public override bool HandleRequest(ISyncPlayStateContext context, bool newState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
{
// Change state
- var pausedState = new PausedGroupState();
- context.SetState(pausedState);
- return pausedState.HandleRequest(context, true, request, session, cancellationToken);
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
}
///
- public override bool HandleRequest(ISyncPlayStateContext context, bool newState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, BufferGroupRequest 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);
+ if (IgnoreBuffering)
+ {
+ return;
+ }
- return true;
+ // Change state
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ if (prevState.Equals(GetGroupState()))
+ {
+ // Group was not waiting, make sure client has latest state
+ var command = context.NewSyncPlayCommand(SendCommandType.Unpause);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+ }
+ else if (prevState.Equals(GroupState.Waiting))
+ {
+ // Notify relevant state change event
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+ }
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, NextTrackGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PreviousTrackGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
}
}
}
diff --git a/Emby.Server.Implementations/SyncPlay/GroupStates/WaitingGroupState.cs b/Emby.Server.Implementations/SyncPlay/GroupStates/WaitingGroupState.cs
new file mode 100644
index 0000000000..9d839b268b
--- /dev/null
+++ b/Emby.Server.Implementations/SyncPlay/GroupStates/WaitingGroupState.cs
@@ -0,0 +1,653 @@
+using System;
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ ///
+ /// Class WaitingGroupState.
+ ///
+ ///
+ /// Class is not thread-safe, external locking is required when accessing methods.
+ ///
+ public class WaitingGroupState : AbstractGroupState
+ {
+ ///
+ /// Tells the state to switch to after buffering is done.
+ ///
+ public bool ResumePlaying { get; set; } = false;
+
+ ///
+ /// Whether the initial state has been set.
+ ///
+ private bool InitialStateSet { get; set; } = false;
+
+ ///
+ /// The group state before the first ever event.
+ ///
+ private GroupState InitialState { get; set; }
+
+ ///
+ /// Default constructor.
+ ///
+ public WaitingGroupState(ILogger logger) : base(logger)
+ {
+ // Do nothing
+ }
+
+ ///
+ public override GroupState GetGroupState()
+ {
+ return GroupState.Waiting;
+ }
+
+ ///
+ public override void SessionJoined(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ if (prevState.Equals(GroupState.Playing)) {
+ ResumePlaying = true;
+ // Pause group and compute the media playback position
+ var currentTime = DateTime.UtcNow;
+ var elapsedTime = currentTime - context.LastActivity;
+ context.LastActivity = currentTime;
+ // Seek only if playback actually started
+ // Event may happen during the delay added to account for latency
+ context.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
+ }
+
+ // Prepare new session
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, update, cancellationToken);
+
+ context.SetBuffering(session, true);
+
+ // Send pause command to all non-buffering sessions
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ context.SendCommand(session, SyncPlayBroadcastType.AllReady, command, cancellationToken);
+ }
+
+ ///
+ public override void SessionLeaving(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ context.SetBuffering(session, false);
+
+ if (!context.IsBuffering())
+ {
+ if (ResumePlaying)
+ {
+ // Client, that was buffering, left the group
+ var playingState = new PlayingGroupState(_logger);
+ context.SetState(playingState);
+ var unpauseRequest = new UnpauseGroupRequest();
+ playingState.HandleRequest(context, GetGroupState(), unpauseRequest, session, cancellationToken);
+
+ _logger.LogDebug("SessionLeaving: {0} left the group {1}, notifying others to resume.", session.Id.ToString(), context.GroupId.ToString());
+ }
+ else
+ {
+ // Group is ready, returning to previous state
+ var pausedState = new PausedGroupState(_logger);
+ context.SetState(pausedState);
+
+ _logger.LogDebug("SessionLeaving: {0} left the group {1}, returning to previous state.", session.Id.ToString(), context.GroupId.ToString());
+ }
+ }
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ ResumePlaying = true;
+
+ var setQueueStatus = context.SetPlayQueue(request.PlayingQueue, request.PlayingItemPosition, request.StartPositionTicks);
+ if (!setQueueStatus)
+ {
+ _logger.LogError("HandleRequest: {0} in group {1}, unable to set playing queue.", request.GetRequestType(), context.GroupId.ToString());
+
+ // Ignore request and return to previous state
+ ISyncPlayState newState;
+ switch (prevState)
+ {
+ case GroupState.Playing:
+ newState = new PlayingGroupState(_logger);
+ break;
+ case GroupState.Paused:
+ newState = new PausedGroupState(_logger);
+ break;
+ default:
+ newState = new IdleGroupState(_logger);
+ break;
+ }
+
+ context.SetState(newState);
+ return;
+ }
+
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+
+ // Reset status of sessions and await for all Ready events before sending Play command
+ context.SetAllBuffering(true);
+
+ _logger.LogDebug("HandleRequest: {0} in group {1}, {2} set a new play queue.", request.GetRequestType(), context.GroupId.ToString(), session.Id.ToString());
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, SetPlaylistItemGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ ResumePlaying = true;
+
+ var result = context.SetPlayingItem(request.PlaylistItemId);
+ if (result)
+ {
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.SetCurrentItem);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+
+ // Reset status of sessions and await for all Ready events before sending Play command
+ context.SetAllBuffering(true);
+ }
+ else
+ {
+ // Return to old state
+ ISyncPlayState newState;
+ switch (prevState)
+ {
+ case GroupState.Playing:
+ newState = new PlayingGroupState(_logger);
+ break;
+ case GroupState.Paused:
+ newState = new PausedGroupState(_logger);
+ break;
+ default:
+ newState = new IdleGroupState(_logger);
+ break;
+ }
+
+ context.SetState(newState);
+
+ _logger.LogDebug("HandleRequest: {0} in group {1}, unable to change current playing item.", request.GetRequestType(), context.GroupId.ToString());
+ }
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, UnpauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ if (prevState.Equals(GroupState.Idle))
+ {
+ ResumePlaying = true;
+ context.RestartCurrentItem();
+
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+
+ // Reset status of sessions and await for all Ready events before sending Play command
+ context.SetAllBuffering(true);
+
+ _logger.LogDebug("HandleRequest: {0} in group {1}, waiting for all ready events.", request.GetRequestType(), context.GroupId.ToString());
+ }
+ else
+ {
+ if (ResumePlaying)
+ {
+ _logger.LogDebug("HandleRequest: {0} in group {1}, ignoring sessions that are not ready and forcing the playback to start.", request.GetRequestType(), context.GroupId.ToString());
+
+ // An Unpause request is forcing the playback to start, ignoring sessions that are not ready
+ context.SetAllBuffering(false);
+
+ // Change state
+ var playingState = new PlayingGroupState(_logger);
+ playingState.IgnoreBuffering = true;
+ context.SetState(playingState);
+ playingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
+ else
+ {
+ // Group would have gone to paused state, now will go to playing state when ready
+ ResumePlaying = true;
+
+ // Notify relevant state change event
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+ }
+ }
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ // Wait for sessions to be ready, then switch to paused state
+ ResumePlaying = false;
+
+ // Notify relevant state change event
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, StopGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ // Change state
+ var idleState = new IdleGroupState(_logger);
+ context.SetState(idleState);
+ idleState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ if (prevState.Equals(GroupState.Playing))
+ {
+ ResumePlaying = true;
+ }
+ else if(prevState.Equals(GroupState.Paused))
+ {
+ ResumePlaying = false;
+ }
+
+ // Sanitize PositionTicks
+ var ticks = context.SanitizePositionTicks(request.PositionTicks);
+
+ // Seek
+ context.PositionTicks = ticks;
+ context.LastActivity = DateTime.UtcNow;
+
+ var command = context.NewSyncPlayCommand(SendCommandType.Seek);
+ context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken);
+
+ // Reset status of sessions and await for all Ready events before sending Play command
+ context.SetAllBuffering(true);
+
+ // Notify relevant state change event
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ // Make sure the client is playing the correct item
+ if (!request.PlaylistItemId.Equals(context.PlayQueue.GetPlayingItemPlaylistId()))
+ {
+ _logger.LogDebug("HandleRequest: {0} in group {1}, {2} has wrong playlist item.", request.GetRequestType(), context.GroupId.ToString(), session.Id.ToString());
+
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.SetCurrentItem);
+ var updateSession = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
+ context.SetBuffering(session, true);
+
+ return;
+ }
+
+ if (prevState.Equals(GroupState.Playing))
+ {
+ // Resume playback when all ready
+ ResumePlaying = true;
+
+ context.SetBuffering(session, true);
+
+ // Pause group and compute the media playback position
+ var currentTime = DateTime.UtcNow;
+ var elapsedTime = currentTime - context.LastActivity;
+ context.LastActivity = currentTime;
+ context.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
+
+ // Send pause command to all non-buffering sessions
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ context.SendCommand(session, SyncPlayBroadcastType.AllReady, command, cancellationToken);
+ }
+ else if (prevState.Equals(GroupState.Paused))
+ {
+ // Don't resume playback when all ready
+ ResumePlaying = false;
+
+ context.SetBuffering(session, true);
+
+ // Send pause command to buffering session
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+ }
+ else if (prevState.Equals(GroupState.Waiting))
+ {
+ // Another session is now buffering
+ context.SetBuffering(session, true);
+
+ if (!ResumePlaying)
+ {
+ // Force update for this session that should be paused
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+ }
+ }
+
+ // Notify relevant state change event
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ // Make sure the client is playing the correct item
+ if (!request.PlaylistItemId.Equals(context.PlayQueue.GetPlayingItemPlaylistId()))
+ {
+ _logger.LogDebug("HandleRequest: {0} in group {1}, {2} has wrong playlist item.", request.GetRequestType(), context.GroupId.ToString(), session.Id.ToString());
+
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.SetCurrentItem);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, update, cancellationToken);
+ context.SetBuffering(session, true);
+
+ return;
+ }
+
+ var requestTicks = context.SanitizePositionTicks(request.PositionTicks);
+ var currentTime = DateTime.UtcNow;
+ var elapsedTime = currentTime - request.When;
+ if (!request.IsPlaying)
+ {
+ elapsedTime = TimeSpan.Zero;
+ }
+
+ var clientPosition = TimeSpan.FromTicks(requestTicks) + elapsedTime;
+ var delayTicks = context.PositionTicks - clientPosition.Ticks;
+
+ if (delayTicks > TimeSpan.FromSeconds(5).Ticks)
+ {
+ // The client is really behind, other participants will have to wait a lot of time...
+ _logger.LogWarning("HandleRequest: {0} in group {1}, {2} got lost in time.", request.GetRequestType(), context.GroupId.ToString(), session.Id.ToString());
+ }
+
+ if (ResumePlaying)
+ {
+ // Handle case where session reported as ready but in reality
+ // it has no clue of the real position nor the playback state
+ if (!request.IsPlaying && Math.Abs(context.PositionTicks - requestTicks) > TimeSpan.FromSeconds(0.5).Ticks) {
+ // Session not ready at all
+ context.SetBuffering(session, true);
+
+ // Correcting session's position
+ var command = context.NewSyncPlayCommand(SendCommandType.Seek);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+
+ // Notify relevant state change event
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+
+ _logger.LogDebug("HandleRequest: {0} in group {1}, {2} got lost in time, correcting.", request.GetRequestType(), context.GroupId.ToString(), session.Id.ToString());
+ return;
+ }
+
+ // Session is ready
+ context.SetBuffering(session, false);
+
+ if (context.IsBuffering())
+ {
+ // Others are still buffering, tell this client to pause when ready
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ var pauseAtTime = currentTime.AddTicks(delayTicks);
+ command.When = context.DateToUTCString(pauseAtTime);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+
+ _logger.LogDebug("HandleRequest: {0} in group {1}, others still buffering, {2} will pause when ready.", request.GetRequestType(), context.GroupId.ToString(), session.Id.ToString());
+ }
+ else
+ {
+ // If all ready, then start playback
+ // Let other clients resume as soon as the buffering client catches up
+ if (delayTicks > context.GetHighestPing() * 2 * TimeSpan.TicksPerMillisecond)
+ {
+ // Client that was buffering is recovering, notifying others to resume
+ context.LastActivity = currentTime.AddTicks(delayTicks);
+ var command = context.NewSyncPlayCommand(SendCommandType.Unpause);
+ var filter = SyncPlayBroadcastType.AllExceptCurrentSession;
+ if (!request.IsPlaying)
+ {
+ filter = SyncPlayBroadcastType.AllGroup;
+ }
+
+ context.SendCommand(session, filter, command, cancellationToken);
+
+ _logger.LogDebug("HandleRequest: {0} in group {1}, {2} is recovering, notifying others to resume.", request.GetRequestType(), context.GroupId.ToString(), session.Id.ToString());
+ }
+ else
+ {
+ // Client, that was buffering, resumed playback but did not update others in time
+ delayTicks = context.GetHighestPing() * 2 * TimeSpan.TicksPerMillisecond;
+ delayTicks = delayTicks < context.DefaultPing ? context.DefaultPing : delayTicks;
+
+ context.LastActivity = currentTime.AddTicks(delayTicks);
+
+ var command = context.NewSyncPlayCommand(SendCommandType.Unpause);
+ context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken);
+
+ _logger.LogDebug("HandleRequest: {0} in group {1}, {2} resumed playback but did not update others in time.", request.GetRequestType(), context.GroupId.ToString(), session.Id.ToString());
+ }
+
+ // Change state
+ var playingState = new PlayingGroupState(_logger);
+ context.SetState(playingState);
+ playingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
+ }
+ else
+ {
+ // Check that session is really ready, tollerate half second difference to account for player imperfections
+ if (Math.Abs(context.PositionTicks - requestTicks) > TimeSpan.FromSeconds(0.5).Ticks)
+ {
+ // Session still not ready
+ context.SetBuffering(session, true);
+
+ // Session is seeking to wrong position, correcting
+ var command = context.NewSyncPlayCommand(SendCommandType.Seek);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+
+ // Notify relevant state change event
+ SendGroupStateUpdate(context, request, session, cancellationToken);
+
+ _logger.LogDebug("HandleRequest: {0} in group {1}, {2} was seeking to wrong position, correcting.", request.GetRequestType(), context.GroupId.ToString(), session.Id.ToString());
+ return;
+ } else {
+ // Session is ready
+ context.SetBuffering(session, false);
+ }
+
+ if (!context.IsBuffering())
+ {
+ // Group is ready, returning to previous state
+ var pausedState = new PausedGroupState(_logger);
+ context.SetState(pausedState);
+
+ if (InitialState.Equals(GroupState.Playing))
+ {
+ // Group went from playing to waiting state and a pause request occured while waiting
+ var pauserequest = new PauseGroupRequest();
+ pausedState.HandleRequest(context, GetGroupState(), pauserequest, session, cancellationToken);
+ }
+ else if (InitialState.Equals(GroupState.Paused))
+ {
+ pausedState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
+
+ _logger.LogDebug("HandleRequest: {0} in group {1}, {2} is ready, returning to previous state.", request.GetRequestType(), context.GroupId.ToString(), session.Id.ToString());
+ }
+ }
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, NextTrackGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ ResumePlaying = true;
+
+ // Make sure the client knows the playing item, to avoid duplicate requests
+ if (!request.PlaylistItemId.Equals(context.PlayQueue.GetPlayingItemPlaylistId()))
+ {
+ _logger.LogDebug("HandleRequest: {0} in group {1}, client provided the wrong playlist id.", request.GetRequestType(), context.GroupId.ToString());
+ return;
+ }
+
+ var newItem = context.NextItemInQueue();
+ if (newItem)
+ {
+ // Send playing-queue update
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NextTrack);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+
+ // Reset status of sessions and await for all Ready events before sending Play command
+ context.SetAllBuffering(true);
+ }
+ else
+ {
+ // Return to old state
+ ISyncPlayState newState;
+ switch (prevState)
+ {
+ case GroupState.Playing:
+ newState = new PlayingGroupState(_logger);
+ break;
+ case GroupState.Paused:
+ newState = new PausedGroupState(_logger);
+ break;
+ default:
+ newState = new IdleGroupState(_logger);
+ break;
+ }
+
+ context.SetState(newState);
+
+ _logger.LogDebug("HandleRequest: {0} in group {1}, no next track available.", request.GetRequestType(), context.GroupId.ToString());
+ }
+ }
+
+ ///
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PreviousTrackGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Save state if first event
+ if (!InitialStateSet)
+ {
+ InitialState = prevState;
+ InitialStateSet = true;
+ }
+
+ ResumePlaying = true;
+
+ // Make sure the client knows the playing item, to avoid duplicate requests
+ if (!request.PlaylistItemId.Equals(context.PlayQueue.GetPlayingItemPlaylistId()))
+ {
+ _logger.LogDebug("HandleRequest: {0} in group {1}, client provided the wrong playlist id.", request.GetRequestType(), context.GroupId.ToString());
+ return;
+ }
+
+ var newItem = context.PreviousItemInQueue();
+ if (newItem)
+ {
+ // Send playing-queue update
+ var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.PreviousTrack);
+ var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
+
+ // Reset status of sessions and await for all Ready events before sending Play command
+ context.SetAllBuffering(true);
+ }
+ else
+ {
+ // Return to old state
+ ISyncPlayState newState;
+ switch (prevState)
+ {
+ case GroupState.Playing:
+ newState = new PlayingGroupState(_logger);
+ break;
+ case GroupState.Paused:
+ newState = new PausedGroupState(_logger);
+ break;
+ default:
+ newState = new IdleGroupState(_logger);
+ break;
+ }
+
+ context.SetState(newState);
+
+ _logger.LogDebug("HandleRequest: {0} in group {1}, no previous track available.", request.GetRequestType(), context.GroupId.ToString());
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
deleted file mode 100644
index 225be7430d..0000000000
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
+++ /dev/null
@@ -1,282 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-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
-{
- ///
- /// Class SyncPlayController.
- ///
- ///
- /// Class is not thread-safe, external locking is required when accessing methods.
- ///
- public class SyncPlayController : ISyncPlayController, ISyncPlayStateContext
- {
- ///
- /// The session manager.
- ///
- private readonly ISessionManager _sessionManager;
-
- ///
- /// The SyncPlay manager.
- ///
- private readonly ISyncPlayManager _syncPlayManager;
-
- ///
- /// The logger.
- ///
- private readonly ILogger _logger;
-
- ///
- /// The group to manage.
- ///
- private readonly GroupInfo _group = new GroupInfo();
-
- ///
- /// Internal group state.
- ///
- /// The group's state.
- private ISyncPlayState State = new PausedGroupState();
-
- ///
- public GroupInfo GetGroup()
- {
- return _group;
- }
-
- ///
- public void SetState(ISyncPlayState state)
- {
- _logger.LogInformation("SetState: {0} -> {1}.", State.GetGroupState(), state.GetGroupState());
- this.State = state;
- }
-
- ///
- public Guid GetGroupId() => _group.GroupId;
-
- ///
- public Guid GetPlayingItemId() => _group.PlayingItem.Id;
-
- ///
- public bool IsGroupEmpty() => _group.IsEmpty();
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The session manager.
- /// The SyncPlay manager.
- public SyncPlayController(
- ISessionManager sessionManager,
- ISyncPlayManager syncPlayManager,
- ILogger logger)
- {
- _sessionManager = sessionManager;
- _syncPlayManager = syncPlayManager;
- _logger = logger;
- }
-
- ///
- /// Filters sessions of this group.
- ///
- /// The current session.
- /// The filtering type.
- /// The array of sessions matching the filter.
- private SessionInfo[] FilterSessions(SessionInfo from, SyncPlayBroadcastType type)
- {
- switch (type)
- {
- case SyncPlayBroadcastType.CurrentSession:
- return new SessionInfo[] { from };
- 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();
- }
- }
-
- ///
- public Task SendGroupUpdate(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate message, CancellationToken cancellationToken)
- {
- IEnumerable GetTasks()
- {
- foreach (var session in FilterSessions(from, type))
- {
- yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id, message, cancellationToken);
- }
- }
-
- return Task.WhenAll(GetTasks());
- }
-
- ///
- public Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken cancellationToken)
- {
- IEnumerable GetTasks()
- {
- foreach (var session in FilterSessions(from, type))
- {
- yield return _sessionManager.SendSyncPlayCommand(session.Id, message, cancellationToken);
- }
- }
-
- return Task.WhenAll(GetTasks());
- }
-
- ///
- public SendCommand NewSyncPlayCommand(SendCommandType type)
- {
- return new SendCommand()
- {
- GroupId = _group.GroupId.ToString(),
- Command = type,
- PositionTicks = _group.PositionTicks,
- When = DateToUTCString(_group.LastActivity),
- EmittedAt = DateToUTCString(DateTime.UtcNow)
- };
- }
-
- ///
- public GroupUpdate NewSyncPlayGroupUpdate(GroupUpdateType type, T data)
- {
- return new GroupUpdate()
- {
- GroupId = _group.GroupId.ToString(),
- Type = type,
- Data = data
- };
- }
-
- ///
- public string DateToUTCString(DateTime _date)
- {
- return _date.ToUniversalTime().ToString("o");
- }
-
- ///
- 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;
- }
-
- ///
- public void CreateGroup(SessionInfo session, CancellationToken cancellationToken)
- {
- _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
- }
-
- ///
- public GroupInfoDto GetInfo()
- {
- return new GroupInfoDto()
- {
- GroupId = GetGroupId().ToString(),
- PlayingItemName = _group.PlayingItem.Name,
- PlayingItemId = _group.PlayingItem.Id.ToString(),
- PositionTicks = _group.PositionTicks,
- Participants = _group.Participants.Values.Select(session => session.Session.UserName).Distinct().ToList()
- };
- }
- }
-}
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
index b85f3c1496..a8e30a9eca 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
@@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
using System.Threading;
-using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
@@ -41,14 +39,14 @@ namespace Emby.Server.Implementations.SyncPlay
///
/// The map between sessions and groups.
///
- private readonly Dictionary _sessionToGroupMap =
- new Dictionary(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary _sessionToGroupMap =
+ new Dictionary(StringComparer.OrdinalIgnoreCase);
///
/// The groups.
///
- private readonly Dictionary _groups =
- new Dictionary();
+ private readonly Dictionary _groups =
+ new Dictionary();
///
/// Lock used for accesing any group.
@@ -75,7 +73,9 @@ namespace Emby.Server.Implementations.SyncPlay
_sessionManager = sessionManager;
_libraryManager = libraryManager;
+ _sessionManager.SessionStarted += OnSessionManagerSessionStarted;
_sessionManager.SessionEnded += OnSessionManagerSessionEnded;
+ _sessionManager.PlaybackStart += OnSessionManagerPlaybackStart;
_sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped;
}
@@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.SyncPlay
/// Gets all groups.
///
/// All groups.
- public IEnumerable Groups => _groups.Values;
+ public IEnumerable Groups => _groups.Values;
///
public void Dispose()
@@ -103,12 +103,30 @@ namespace Emby.Server.Implementations.SyncPlay
return;
}
+ _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
_sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
+ _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
_sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
_disposed = true;
}
+ private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e)
+ {
+ var session = e.SessionInfo;
+ if (!IsSessionInGroup(session))
+ {
+ return;
+ }
+
+ var groupId = GetSessionGroup(session) ?? Guid.Empty;
+ var request = new JoinGroupRequest()
+ {
+ GroupId = groupId
+ };
+ JoinGroup(session, groupId, request, CancellationToken.None);
+ }
+
private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
{
var session = e.SessionInfo;
@@ -117,7 +135,18 @@ namespace Emby.Server.Implementations.SyncPlay
return;
}
- LeaveGroup(session, CancellationToken.None);
+ // TODO: probably remove this event, not used at the moment
+ }
+
+ private void OnSessionManagerPlaybackStart(object sender, PlaybackProgressEventArgs e)
+ {
+ var session = e.Session;
+ if (!IsSessionInGroup(session))
+ {
+ return;
+ }
+
+ // TODO: probably remove this event, not used at the moment
}
private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e)
@@ -128,7 +157,7 @@ namespace Emby.Server.Implementations.SyncPlay
return;
}
- LeaveGroup(session, CancellationToken.None);
+ // TODO: probably remove this event, not used at the moment
}
private bool IsSessionInGroup(SessionInfo session)
@@ -136,33 +165,14 @@ namespace Emby.Server.Implementations.SyncPlay
return _sessionToGroupMap.ContainsKey(session.Id);
}
- private bool HasAccessToItem(User user, Guid itemId)
- {
- var item = _libraryManager.GetItemById(itemId);
-
- // Check ParentalRating access
- var hasParentalRatingAccess = !user.MaxParentalAgeRating.HasValue
- || item.InheritedParentalRatingValue <= user.MaxParentalAgeRating;
-
- if (!user.HasPermission(PermissionKind.EnableAllFolders) && hasParentalRatingAccess)
- {
- var collections = _libraryManager.GetCollectionFolders(item).Select(
- folder => folder.Id.ToString("N", CultureInfo.InvariantCulture));
-
- return collections.Intersect(user.GetPreference(PreferenceKind.EnabledFolders)).Any();
- }
-
- return hasParentalRatingAccess;
- }
-
private Guid? GetSessionGroup(SessionInfo session)
{
_sessionToGroupMap.TryGetValue(session.Id, out var group);
- return group?.GetGroupId();
+ return group?.GroupId;
}
///
- public void NewGroup(SessionInfo session, CancellationToken cancellationToken)
+ public void NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken)
{
var user = _userManager.GetUserById(session.UserId);
@@ -174,8 +184,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.CreateGroupDenied
};
-
- _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
return;
}
@@ -186,10 +195,10 @@ namespace Emby.Server.Implementations.SyncPlay
LeaveGroup(session, cancellationToken);
}
- var group = new SyncPlayController(_sessionManager, this, _logger);
- _groups[group.GetGroupId()] = group;
+ var group = new SyncPlayGroupController(_logger, _userManager, _sessionManager, _libraryManager, this);
+ _groups[group.GroupId] = group;
- group.CreateGroup(session, cancellationToken);
+ group.CreateGroup(session, request, cancellationToken);
}
}
@@ -206,14 +215,13 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.JoinGroupDenied
};
-
- _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
return;
}
lock (_groupsLock)
{
- ISyncPlayController group;
+ ISyncPlayGroupController group;
_groups.TryGetValue(groupId, out group);
if (group == null)
@@ -224,20 +232,20 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.GroupDoesNotExist
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
return;
}
- if (!HasAccessToItem(user, group.GetPlayingItemId()))
+ if (!group.HasAccessToPlayQueue(user))
{
- _logger.LogWarning("JoinGroup: {0} does not have access to {1}.", session.Id, group.GetPlayingItemId());
+ _logger.LogWarning("JoinGroup: {0} does not have access to some content from the playing queue of group {1}.", session.Id, group.GroupId.ToString());
var error = new GroupUpdate()
{
- GroupId = group.GetGroupId().ToString(),
+ GroupId = group.GroupId.ToString(),
Type = GroupUpdateType.LibraryAccessDenied
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
return;
}
@@ -245,6 +253,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
if (GetSessionGroup(session).Equals(groupId))
{
+ group.SessionRestore(session, request, cancellationToken);
return;
}
@@ -271,7 +280,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.NotInGroup
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
return;
}
@@ -279,14 +288,14 @@ namespace Emby.Server.Implementations.SyncPlay
if (group.IsGroupEmpty())
{
- _logger.LogInformation("LeaveGroup: removing empty group {0}.", group.GetGroupId());
- _groups.Remove(group.GetGroupId(), out _);
+ _logger.LogInformation("LeaveGroup: removing empty group {0}.", group.GroupId);
+ _groups.Remove(group.GroupId, out _);
}
}
}
///
- public List ListGroups(SessionInfo session, Guid filterItemId)
+ public List ListGroups(SessionInfo session)
{
var user = _userManager.GetUserById(session.UserId);
@@ -295,20 +304,9 @@ namespace Emby.Server.Implementations.SyncPlay
return new List();
}
- // Filter by item if requested
- if (!filterItemId.Equals(Guid.Empty))
- {
- return _groups.Values.Where(
- group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId())).Select(
- group => group.GetInfo()).ToList();
- }
- else
- {
- // Otherwise show all available groups
- return _groups.Values.Where(
- group => HasAccessToItem(user, group.GetPlayingItemId())).Select(
- group => group.GetInfo()).ToList();
- }
+ return _groups.Values.Where(
+ group => group.HasAccessToPlayQueue(user)).Select(
+ group => group.GetInfo()).ToList();
}
///
@@ -324,8 +322,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.JoinGroupDenied
};
-
- _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
return;
}
@@ -341,7 +338,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.NotInGroup
};
- _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
+ _sessionManager.SendSyncPlayGroupUpdate(session, error, CancellationToken.None);
return;
}
@@ -350,7 +347,7 @@ namespace Emby.Server.Implementations.SyncPlay
}
///
- public void AddSessionToGroup(SessionInfo session, ISyncPlayController group)
+ public void AddSessionToGroup(SessionInfo session, ISyncPlayGroupController group)
{
if (IsSessionInGroup(session))
{
@@ -361,7 +358,7 @@ namespace Emby.Server.Implementations.SyncPlay
}
///
- public void RemoveSessionFromGroup(SessionInfo session, ISyncPlayController group)
+ public void RemoveSessionFromGroup(SessionInfo session, ISyncPlayGroupController group)
{
if (!IsSessionInGroup(session))
{
@@ -369,7 +366,7 @@ namespace Emby.Server.Implementations.SyncPlay
}
_sessionToGroupMap.Remove(session.Id, out var tempGroup);
- if (!tempGroup.GetGroupId().Equals(group.GetGroupId()))
+ if (!tempGroup.GroupId.Equals(group.GroupId))
{
throw new InvalidOperationException("Session was in wrong group!");
}
diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index e16a10ba4d..847c3ab117 100644
--- a/Jellyfin.Api/Controllers/SyncPlayController.cs
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -43,14 +43,20 @@ namespace Jellyfin.Api.Controllers
///
/// Create a new SyncPlay group.
///
+ /// The name of the new group.
/// New group created.
/// A indicating success.
[HttpPost("New")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SyncPlayCreateGroup()
+ public ActionResult SyncPlayCreateGroup(
+ [FromQuery, Required] string groupName)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
- _syncPlayManager.NewGroup(currentSession, CancellationToken.None);
+ var newGroupRequest = new NewGroupRequest()
+ {
+ GroupName = groupName
+ };
+ _syncPlayManager.NewGroup(currentSession, newGroupRequest, CancellationToken.None);
return NoContent();
}
@@ -62,15 +68,14 @@ namespace Jellyfin.Api.Controllers
/// A indicating success.
[HttpPost("Join")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SyncPlayJoinGroup([FromQuery, Required] Guid groupId)
+ public ActionResult SyncPlayJoinGroup(
+ [FromQuery, Required] Guid groupId)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
-
var joinRequest = new JoinGroupRequest()
{
GroupId = groupId
};
-
_syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None);
return NoContent();
}
@@ -92,35 +97,143 @@ namespace Jellyfin.Api.Controllers
///
/// Gets all SyncPlay groups.
///
- /// Optional. Filter by item id.
/// Groups returned.
/// An containing the available SyncPlay groups.
[HttpGet("List")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult> SyncPlayGetGroups([FromQuery] Guid? filterItemId)
+ public ActionResult> SyncPlayGetGroups()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
- return Ok(_syncPlayManager.ListGroups(currentSession, filterItemId.HasValue ? filterItemId.Value : Guid.Empty));
+ return Ok(_syncPlayManager.ListGroups(currentSession));
}
///
/// Request play in SyncPlay group.
///
+ /// The playing queue. Item ids in the playing queue, comma delimited.
+ /// The playing item position from the queue.
+ /// The start position ticks.
/// Play request sent to all group members.
/// A indicating success.
[HttpPost("Play")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SyncPlayPlay()
+ public ActionResult SyncPlayPlay(
+ [FromQuery, Required] string playingQueue,
+ [FromQuery, Required] int playingItemPosition,
+ [FromQuery, Required] long startPositionTicks)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
- var syncPlayRequest = new PlaybackRequest()
+ var syncPlayRequest = new PlayGroupRequest()
{
- Type = PlaybackRequestType.Play
+ PlayingQueue = RequestHelpers.GetGuids(playingQueue),
+ PlayingItemPosition = playingItemPosition,
+ StartPositionTicks = startPositionTicks
};
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
+ ///
+ /// Request to change playlist item in SyncPlay group.
+ ///
+ /// The playlist id of the item.
+ /// Queue update request sent to all group members.
+ /// A indicating success.
+ [HttpPost("SetPlaylistItem")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlaySetPlaylistItem(
+ [FromQuery, Required] string playlistItemId)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new SetPlaylistItemGroupRequest()
+ {
+ PlaylistItemId = playlistItemId
+ };
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ ///
+ /// Request to remove items from the playlist in SyncPlay group.
+ ///
+ /// The playlist ids of the items to remove.
+ /// Queue update request sent to all group members.
+ /// A indicating success.
+ [HttpPost("RemoveFromPlaylist")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayRemoveFromPlaylist(
+ [FromQuery, Required] string[] playlistItemIds)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new RemoveFromPlaylistGroupRequest()
+ {
+ PlaylistItemIds = playlistItemIds
+ };
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ ///
+ /// Request to move an item in the playlist in SyncPlay group.
+ ///
+ /// The playlist id of the item to move.
+ /// The new position.
+ /// Queue update request sent to all group members.
+ /// A indicating success.
+ [HttpPost("MovePlaylistItem")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayMovePlaylistItem(
+ [FromQuery, Required] string playlistItemId,
+ [FromQuery, Required] int newIndex)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new MovePlaylistItemGroupRequest()
+ {
+ PlaylistItemId = playlistItemId,
+ NewIndex = newIndex
+ };
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ ///
+ /// Request to queue items to the playlist of a SyncPlay group.
+ ///
+ /// The items to add. Item ids, comma delimited.
+ /// The mode in which to queue items.
+ /// Queue update request sent to all group members.
+ /// A indicating success.
+ [HttpPost("Queue")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayQueue(
+ [FromQuery, Required] string itemIds,
+ [FromQuery, Required] string mode)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new QueueGroupRequest()
+ {
+ ItemIds = RequestHelpers.GetGuids(itemIds),
+ Mode = mode
+ };
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ ///
+ /// Request unpause in SyncPlay group.
+ ///
+ /// Unpause request sent to all group members.
+ /// A indicating success.
+ [HttpPost("Unpause")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayUnpause()
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new UnpauseGroupRequest();
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
///
/// Request pause in SyncPlay group.
///
@@ -131,10 +244,22 @@ namespace Jellyfin.Api.Controllers
public ActionResult SyncPlayPause()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
- var syncPlayRequest = new PlaybackRequest()
- {
- Type = PlaybackRequestType.Pause
- };
+ var syncPlayRequest = new PauseGroupRequest();
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ ///
+ /// Request stop in SyncPlay group.
+ ///
+ /// Stop request sent to all group members.
+ /// A indicating success.
+ [HttpPost("Stop")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayStop()
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new StopGroupRequest();
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
}
@@ -147,12 +272,12 @@ namespace Jellyfin.Api.Controllers
/// A indicating success.
[HttpPost("Seek")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SyncPlaySeek([FromQuery] long positionTicks)
+ public ActionResult SyncPlaySeek(
+ [FromQuery, Required] long positionTicks)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
- var syncPlayRequest = new PlaybackRequest()
+ var syncPlayRequest = new SeekGroupRequest()
{
- Type = PlaybackRequestType.Seek,
PositionTicks = positionTicks
};
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
@@ -164,19 +289,142 @@ namespace Jellyfin.Api.Controllers
///
/// When the request has been made by the client.
/// The playback position in ticks.
+ /// Whether the client's playback is playing or not.
+ /// The playlist item id.
/// Whether the buffering is done.
/// Buffering request sent to all group members.
/// A indicating success.
[HttpPost("Buffering")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SyncPlayBuffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone)
+ public ActionResult SyncPlayBuffering(
+ [FromQuery, Required] DateTime when,
+ [FromQuery, Required] long positionTicks,
+ [FromQuery, Required] bool isPlaying,
+ [FromQuery, Required] string playlistItemId,
+ [FromQuery, Required] bool bufferingDone)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
- var syncPlayRequest = new PlaybackRequest()
+ IPlaybackGroupRequest syncPlayRequest;
+ if (!bufferingDone)
{
- Type = bufferingDone ? PlaybackRequestType.Ready : PlaybackRequestType.Buffer,
- When = when,
- PositionTicks = positionTicks
+ syncPlayRequest = new BufferGroupRequest()
+ {
+ When = when,
+ PositionTicks = positionTicks,
+ IsPlaying = isPlaying,
+ PlaylistItemId = playlistItemId
+ };
+ }
+ else
+ {
+ syncPlayRequest = new ReadyGroupRequest()
+ {
+ When = when,
+ PositionTicks = positionTicks,
+ IsPlaying = isPlaying,
+ PlaylistItemId = playlistItemId
+ };
+ }
+
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ ///
+ /// Request SyncPlay group to ignore member during group-wait.
+ ///
+ /// Whether to ignore the member.
+ /// Member state updated.
+ /// A indicating success.
+ [HttpPost("SetIgnoreWait")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlaySetIgnoreWait(
+ [FromQuery, Required] bool ignoreWait)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new IgnoreWaitGroupRequest()
+ {
+ IgnoreWait = ignoreWait
+ };
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ ///
+ /// Request next track in SyncPlay group.
+ ///
+ /// The playing item id.
+ /// Next track request sent to all group members.
+ /// A indicating success.
+ [HttpPost("NextTrack")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayNextTrack(
+ [FromQuery, Required] string playlistItemId)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new NextTrackGroupRequest()
+ {
+ PlaylistItemId = playlistItemId
+ };
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ ///
+ /// Request previous track in SyncPlay group.
+ ///
+ /// The playing item id.
+ /// Previous track request sent to all group members.
+ /// A indicating success.
+ [HttpPost("PreviousTrack")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlayPreviousTrack(
+ [FromQuery, Required] string playlistItemId)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new PreviousTrackGroupRequest()
+ {
+ PlaylistItemId = playlistItemId
+ };
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ ///
+ /// Request to set repeat mode in SyncPlay group.
+ ///
+ /// The repeat mode.
+ /// Play queue update sent to all group members.
+ /// A indicating success.
+ [HttpPost("SetRepeatMode")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlaySetRepeatMode(
+ [FromQuery, Required] string mode)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new SetRepeatModeGroupRequest()
+ {
+ Mode = mode
+ };
+ _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
+ return NoContent();
+ }
+
+ ///
+ /// Request to set shuffle mode in SyncPlay group.
+ ///
+ /// The shuffle mode.
+ /// Play queue update sent to all group members.
+ /// A indicating success.
+ [HttpPost("SetShuffleMode")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult SyncPlaySetShuffleMode(
+ [FromQuery, Required] string mode)
+ {
+ var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
+ var syncPlayRequest = new SetShuffleModeGroupRequest()
+ {
+ Mode = mode
};
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
return NoContent();
@@ -190,12 +438,12 @@ namespace Jellyfin.Api.Controllers
/// A indicating success.
[HttpPost("Ping")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult SyncPlayPing([FromQuery] double ping)
+ public ActionResult SyncPlayPing(
+ [FromQuery, Required] double ping)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
- var syncPlayRequest = new PlaybackRequest()
+ var syncPlayRequest = new PingGroupRequest()
{
- Type = PlaybackRequestType.Ping,
Ping = Convert.ToInt64(ping)
};
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
diff --git a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs
deleted file mode 100644
index bd9670f07a..0000000000
--- a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs
+++ /dev/null
@@ -1,267 +0,0 @@
-using System.Threading;
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Controller.SyncPlay;
-using MediaBrowser.Model.Services;
-using MediaBrowser.Model.SyncPlay;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Api.SyncPlay
-{
- [Route("/SyncPlay/New", "POST", Summary = "Create a new SyncPlay group")]
- [Authenticated]
- public class SyncPlayNew : IReturnVoid
- {
- }
-
- [Route("/SyncPlay/Join", "POST", Summary = "Join an existing SyncPlay group")]
- [Authenticated]
- public class SyncPlayJoin : IReturnVoid
- {
- ///
- /// Gets or sets the Group id.
- ///
- /// The Group id to join.
- [ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string GroupId { get; set; }
- }
-
- [Route("/SyncPlay/Leave", "POST", Summary = "Leave joined SyncPlay group")]
- [Authenticated]
- public class SyncPlayLeave : IReturnVoid
- {
- }
-
- [Route("/SyncPlay/List", "GET", Summary = "List SyncPlay groups")]
- [Authenticated]
- public class SyncPlayList : IReturnVoid
- {
- ///
- /// Gets or sets the filter item id.
- ///
- /// The filter item id.
- [ApiMember(Name = "FilterItemId", Description = "Filter by item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
- public string FilterItemId { get; set; }
- }
-
- [Route("/SyncPlay/Play", "POST", Summary = "Request play in SyncPlay group")]
- [Authenticated]
- public class SyncPlayPlay : IReturnVoid
- {
- }
-
- [Route("/SyncPlay/Pause", "POST", Summary = "Request pause in SyncPlay group")]
- [Authenticated]
- public class SyncPlayPause : IReturnVoid
- {
- }
-
- [Route("/SyncPlay/Seek", "POST", Summary = "Request seek in SyncPlay group")]
- [Authenticated]
- public class SyncPlaySeek : IReturnVoid
- {
- [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")]
- public long PositionTicks { get; set; }
- }
-
- [Route("/SyncPlay/Buffering", "POST", Summary = "Request group wait in SyncPlay group while buffering")]
- [Authenticated]
- public class SyncPlayBuffering : IReturnVoid
- {
- ///
- /// Gets or sets the date used to pin PositionTicks in time.
- ///
- /// The date related to PositionTicks.
- [ApiMember(Name = "When", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
- public string When { get; set; }
-
- [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")]
- public long PositionTicks { get; set; }
-
- ///
- /// Gets or sets whether this is a buffering or a ready request.
- ///
- /// true if buffering is complete; false otherwise.
- [ApiMember(Name = "BufferingDone", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")]
- public bool BufferingDone { get; set; }
- }
-
- [Route("/SyncPlay/Ping", "POST", Summary = "Update session ping")]
- [Authenticated]
- public class SyncPlayPing : IReturnVoid
- {
- [ApiMember(Name = "Ping", IsRequired = true, DataType = "double", ParameterType = "query", Verb = "POST")]
- public double Ping { get; set; }
- }
-
- ///
- /// Class SyncPlayService.
- ///
- public class SyncPlayService : BaseApiService
- {
- ///
- /// The session context.
- ///
- private readonly ISessionContext _sessionContext;
-
- ///
- /// The SyncPlay manager.
- ///
- private readonly ISyncPlayManager _syncPlayManager;
-
- public SyncPlayService(
- ILogger logger,
- IServerConfigurationManager serverConfigurationManager,
- IHttpResultFactory httpResultFactory,
- ISessionContext sessionContext,
- ISyncPlayManager syncPlayManager)
- : base(logger, serverConfigurationManager, httpResultFactory)
- {
- _sessionContext = sessionContext;
- _syncPlayManager = syncPlayManager;
- }
-
- ///
- /// Handles the specified request.
- ///
- /// The request.
- public void Post(SyncPlayNew request)
- {
- var currentSession = GetSession(_sessionContext);
- _syncPlayManager.NewGroup(currentSession, CancellationToken.None);
- }
-
- ///
- /// Handles the specified request.
- ///
- /// The request.
- public void Post(SyncPlayJoin request)
- {
- var currentSession = GetSession(_sessionContext);
-
- Guid groupId;
- if (!Guid.TryParse(request.GroupId, out groupId))
- {
- Logger.LogError("JoinGroup: {0} is not a valid format for GroupId. Ignoring request.", request.GroupId);
- return;
- }
-
- var joinRequest = new JoinGroupRequest()
- {
- GroupId = groupId
- };
-
- _syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None);
- }
-
- ///
- /// Handles the specified request.
- ///
- /// The request.
- public void Post(SyncPlayLeave request)
- {
- var currentSession = GetSession(_sessionContext);
- _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None);
- }
-
- ///
- /// Handles the specified request.
- ///
- /// The request.
- /// The requested list of groups.
- public List Get(SyncPlayList request)
- {
- var currentSession = GetSession(_sessionContext);
- var filterItemId = Guid.Empty;
-
- if (!string.IsNullOrEmpty(request.FilterItemId) && !Guid.TryParse(request.FilterItemId, out filterItemId))
- {
- Logger.LogWarning("ListGroups: {0} is not a valid format for FilterItemId. Ignoring filter.", request.FilterItemId);
- }
-
- return _syncPlayManager.ListGroups(currentSession, filterItemId);
- }
-
- ///
- /// Handles the specified request.
- ///
- /// The request.
- public void Post(SyncPlayPlay request)
- {
- var currentSession = GetSession(_sessionContext);
- var syncPlayRequest = new PlayGroupRequest();
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- }
-
- ///
- /// Handles the specified request.
- ///
- /// The request.
- public void Post(SyncPlayPause request)
- {
- var currentSession = GetSession(_sessionContext);
- var syncPlayRequest = new PauseGroupRequest();
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- }
-
- ///
- /// Handles the specified request.
- ///
- /// The request.
- public void Post(SyncPlaySeek request)
- {
- var currentSession = GetSession(_sessionContext);
- var syncPlayRequest = new SeekGroupRequest()
- {
- PositionTicks = request.PositionTicks
- };
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- }
-
- ///
- /// Handles the specified request.
- ///
- /// The request.
- public void Post(SyncPlayBuffering request)
- {
- var currentSession = GetSession(_sessionContext);
-
- IPlaybackGroupRequest syncPlayRequest;
- if (!request.BufferingDone)
- {
- 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);
- }
-
- ///
- /// Handles the specified request.
- ///
- /// The request.
- public void Post(SyncPlayPing request)
- {
- var currentSession = GetSession(_sessionContext);
- var syncPlayRequest = new PingGroupRequest()
- {
- Ping = Convert.ToInt64(request.Ping)
- };
- _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
- }
- }
-}
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 04c3004ee6..9ad8557ce6 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -143,22 +143,22 @@ namespace MediaBrowser.Controller.Session
Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken);
///
- /// Sends the SyncPlayCommand.
+ /// Sends a SyncPlayCommand to a session.
///
- /// The session id.
+ /// The session.
/// The command.
/// The cancellation token.
/// Task.
- Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken);
+ Task SendSyncPlayCommand(SessionInfo session, SendCommand command, CancellationToken cancellationToken);
///
- /// Sends the SyncPlayGroupUpdate.
+ /// Sends a SyncPlayGroupUpdate to a session.
///
- /// The session id.
+ /// The session.
/// The group update.
/// The cancellation token.
/// Task.
- Task SendSyncPlayGroupUpdate(string sessionId, GroupUpdate command, CancellationToken cancellationToken);
+ Task SendSyncPlayGroupUpdate(SessionInfo session, GroupUpdate command, CancellationToken cancellationToken);
///
/// Sends the browse command.
diff --git a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
deleted file mode 100644
index cdd24d0b59..0000000000
--- a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
+++ /dev/null
@@ -1,154 +0,0 @@
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Session;
-
-namespace MediaBrowser.Controller.SyncPlay
-{
- ///
- /// Class GroupInfo.
- ///
- ///
- /// Class is not thread-safe, external locking is required when accessing methods.
- ///
- public class GroupInfo
- {
- ///
- /// The default ping value used for sessions.
- ///
- public const long DefaultPing = 500;
-
- ///
- /// Gets the group identifier.
- ///
- /// The group identifier.
- public Guid GroupId { get; } = Guid.NewGuid();
-
- ///
- /// Gets or sets the playing item.
- ///
- /// The playing item.
- public BaseItem PlayingItem { get; set; }
-
- ///
- /// Gets or sets a value indicating whether there are position ticks.
- ///
- /// The position ticks.
- public long PositionTicks { get; set; }
-
- ///
- /// Gets or sets the last activity.
- ///
- /// The last activity.
- public DateTime LastActivity { get; set; }
-
- ///
- /// Gets the participants.
- ///
- /// The participants, or members of the group.
- public Dictionary Participants { get; } =
- new Dictionary(StringComparer.OrdinalIgnoreCase);
-
- ///
- /// Checks if a session is in this group.
- ///
- /// The session id to check.
- /// true if the session is in this group; false otherwise.
- public bool ContainsSession(string sessionId)
- {
- return Participants.ContainsKey(sessionId);
- }
-
- ///
- /// Adds the session to the group.
- ///
- /// The session.
- public void AddSession(SessionInfo session)
- {
- Participants.TryAdd(
- session.Id,
- new GroupMember
- {
- Session = session,
- Ping = DefaultPing,
- IsBuffering = false
- });
- }
-
- ///
- /// Removes the session from the group.
- ///
- /// The session.
- public void RemoveSession(SessionInfo session)
- {
- Participants.Remove(session.Id);
- }
-
- ///
- /// Updates the ping of a session.
- ///
- /// The session.
- /// The ping.
- public void UpdatePing(SessionInfo session, long ping)
- {
- if (Participants.TryGetValue(session.Id, out GroupMember value))
- {
- value.Ping = ping;
- }
- }
-
- ///
- /// Gets the highest ping in the group.
- ///
- /// The highest ping in the group.
- public long GetHighestPing()
- {
- long max = long.MinValue;
- foreach (var session in Participants.Values)
- {
- max = Math.Max(max, session.Ping);
- }
-
- return max;
- }
-
- ///
- /// Sets the session's buffering state.
- ///
- /// The session.
- /// The state.
- public void SetBuffering(SessionInfo session, bool isBuffering)
- {
- if (Participants.TryGetValue(session.Id, out GroupMember value))
- {
- value.IsBuffering = isBuffering;
- }
- }
-
- ///
- /// Gets the group buffering state.
- ///
- /// true if there is a session buffering in the group; false otherwise.
- public bool IsBuffering()
- {
- foreach (var session in Participants.Values)
- {
- if (session.IsBuffering)
- {
- return true;
- }
- }
-
- return false;
- }
-
- ///
- /// Checks if the group is empty.
- ///
- /// true if the group is empty; false otherwise.
- public bool IsEmpty()
- {
- return Participants.Count == 0;
- }
- }
-}
diff --git a/MediaBrowser.Controller/SyncPlay/GroupMember.cs b/MediaBrowser.Controller/SyncPlay/GroupMember.cs
index cde6f8e8ce..9a9d30277f 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupMember.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupMember.cs
@@ -7,12 +7,6 @@ namespace MediaBrowser.Controller.SyncPlay
///
public class GroupMember
{
- ///
- /// Gets or sets a value indicating whether this member is buffering.
- ///
- /// true if member is buffering; false otherwise.
- public bool IsBuffering { get; set; }
-
///
/// Gets or sets the session.
///
@@ -20,9 +14,21 @@ namespace MediaBrowser.Controller.SyncPlay
public SessionInfo Session { get; set; }
///
- /// Gets or sets the ping.
+ /// Gets or sets the ping, in milliseconds.
///
/// The ping.
public long Ping { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether this member is buffering.
+ ///
+ /// true if member is buffering; false otherwise.
+ public bool IsBuffering { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether this member is following group playback.
+ ///
+ /// true to ignore member on group wait; false if they're following group playback.
+ public bool IgnoreGroupWait { get; set; }
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/IPlaybackGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/IPlaybackGroupRequest.cs
index a6e87a007f..35ca64c8df 100644
--- a/MediaBrowser.Controller/SyncPlay/IPlaybackGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/IPlaybackGroupRequest.cs
@@ -12,13 +12,12 @@ namespace MediaBrowser.Controller.SyncPlay
///
/// Gets the playback request type.
///
- /// The playback request type.
- PlaybackRequestType Type();
+ /// The playback request type.
+ PlaybackRequestType GetRequestType();
///
/// Applies the request to a group.
///
- /// The operation completion status.
- bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken);
+ void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs
index 5ac2aeb247..9a4e1ee1ee 100644
--- a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs
+++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs
@@ -1,39 +1,41 @@
using System;
using System.Threading;
+using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.SyncPlay;
namespace MediaBrowser.Controller.SyncPlay
{
///
- /// Interface ISyncPlayController.
+ /// Interface ISyncPlayGroupController.
///
- public interface ISyncPlayController
+ public interface ISyncPlayGroupController
{
///
- /// Gets the group id.
+ /// Gets the group identifier.
///
- /// The group id.
- Guid GetGroupId();
+ /// The group identifier.
+ Guid GroupId { get; }
///
- /// Gets the playing item id.
+ /// Gets the play queue.
///
- /// The playing item id.
- Guid GetPlayingItemId();
+ /// The play queue.
+ PlayQueueManager PlayQueue { get; }
///
/// Checks if the group is empty.
///
- /// If the group is empty.
+ /// If the group is empty.
bool IsGroupEmpty();
///
/// Initializes the group with the session's info.
///
/// The session.
+ /// The request.
/// The cancellation token.
- void CreateGroup(SessionInfo session, CancellationToken cancellationToken);
+ void CreateGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken);
///
/// Adds the session to the group.
@@ -43,6 +45,14 @@ namespace MediaBrowser.Controller.SyncPlay
/// The cancellation token.
void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken);
+ ///
+ /// Restores the state of a session that already joined the group.
+ ///
+ /// The session.
+ /// The request.
+ /// The cancellation token.
+ void SessionRestore(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken);
+
///
/// Removes the session from the group.
///
@@ -61,7 +71,15 @@ namespace MediaBrowser.Controller.SyncPlay
///
/// Gets the info about the group for the clients.
///
- /// The group info for the clients.
+ /// The group info for the clients.
GroupInfoDto GetInfo();
+
+ ///
+ /// Checks if a user has access to all content in the play queue.
+ ///
+ /// The user.
+ /// true if the user can access the play queue; false otherwise.
+ bool HasAccessToPlayQueue(User user);
+
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
index 6fa94e2ce4..9bef3f5593 100644
--- a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
+++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
@@ -15,8 +15,9 @@ namespace MediaBrowser.Controller.SyncPlay
/// Creates a new group.
///
/// The session that's creating the group.
+ /// The request.
/// The cancellation token.
- void NewGroup(SessionInfo session, CancellationToken cancellationToken);
+ void NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken);
///
/// Adds the session to a group.
@@ -38,9 +39,8 @@ namespace MediaBrowser.Controller.SyncPlay
/// Gets list of available groups for a session.
///
/// The session.
- /// The item id to filter by.
- /// The list of available groups.
- List ListGroups(SessionInfo session, Guid filterItemId);
+ /// The list of available groups.
+ List ListGroups(SessionInfo session);
///
/// Handle a request by a session in a group.
@@ -56,7 +56,7 @@ namespace MediaBrowser.Controller.SyncPlay
/// The session.
/// The group.
///
- void AddSessionToGroup(SessionInfo session, ISyncPlayController group);
+ void AddSessionToGroup(SessionInfo session, ISyncPlayGroupController group);
///
/// Unmaps a session from a group.
@@ -64,6 +64,6 @@ namespace MediaBrowser.Controller.SyncPlay
/// The session.
/// The group.
///
- void RemoveSessionFromGroup(SessionInfo session, ISyncPlayController group);
+ void RemoveSessionFromGroup(SessionInfo session, ISyncPlayGroupController group);
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayState.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayState.cs
index 55c9ee938f..290576e30f 100644
--- a/MediaBrowser.Controller/SyncPlay/ISyncPlayState.cs
+++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayState.cs
@@ -15,81 +15,202 @@ namespace MediaBrowser.Controller.SyncPlay
/// The group state.
GroupState GetGroupState();
+ ///
+ /// Handle a session that joined the group.
+ ///
+ /// The context of the state.
+ /// The previous state.
+ /// The session.
+ /// The cancellation token.
+ void SessionJoined(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ ///
+ /// Handle a session that is leaving the group.
+ ///
+ /// The context of the state.
+ /// The previous state.
+ /// The session.
+ /// The cancellation token.
+ void SessionLeaving(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken);
+
///
/// Generic handle. Context's state can change.
///
/// The context of the state.
- /// Whether the state has been just set.
- /// The play action.
+ /// The previous state.
+ /// The generic action.
/// The session.
/// The cancellation token.
- /// The operation completion status.
- bool HandleRequest(ISyncPlayStateContext context, bool newState, IPlaybackGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, 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 previous state.
/// The play action.
/// The session.
/// The cancellation token.
- /// The operation completion status.
- bool HandleRequest(ISyncPlayStateContext context, bool newState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+
+ ///
+ /// Handles a playlist-item change requested by a session. Context's state can change.
+ ///
+ /// The context of the state.
+ /// The previous state.
+ /// The playlist-item change action.
+ /// The session.
+ /// The cancellation token.
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, SetPlaylistItemGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+
+ ///
+ /// Handles a remove-items change requested by a session. Context's state can change.
+ ///
+ /// The context of the state.
+ /// The previous state.
+ /// The remove-items change action.
+ /// The session.
+ /// The cancellation token.
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, RemoveFromPlaylistGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+
+ ///
+ /// Handles a move-item change requested by a session. Context's state should not change.
+ ///
+ /// The context of the state.
+ /// The previous state.
+ /// The move-item change action.
+ /// The session.
+ /// The cancellation token.
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, MovePlaylistItemGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+
+ ///
+ /// Handles a queue change requested by a session. Context's state should not change.
+ ///
+ /// The context of the state.
+ /// The previous state.
+ /// The queue action.
+ /// The session.
+ /// The cancellation token.
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, QueueGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+
+ ///
+ /// Handles an unpause action requested by a session. Context's state can change.
+ ///
+ /// The context of the state.
+ /// The previous state.
+ /// The unpause action.
+ /// The session.
+ /// The cancellation token.
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, UnpauseGroupRequest 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 previous state.
/// The pause action.
/// The session.
/// The cancellation token.
- /// The operation completion status.
- bool HandleRequest(ISyncPlayStateContext context, bool newState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+
+ ///
+ /// Handles a stop action requested by a session. Context's state can change.
+ ///
+ /// The context of the state.
+ /// The previous state.
+ /// The stop action.
+ /// The session.
+ /// The cancellation token.
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, StopGroupRequest 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 previous state.
/// The seek action.
/// The session.
/// The cancellation token.
- /// The operation completion status.
- bool HandleRequest(ISyncPlayStateContext context, bool newState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, 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 previous state.
/// The buffering action.
/// The session.
/// The cancellation token.
- /// The operation completion status.
- bool HandleRequest(ISyncPlayStateContext context, bool newState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, 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 previous state.
/// 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);
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+
+ ///
+ /// Handles a next-track action requested by a session. Context's state can change.
+ ///
+ /// The context of the state.
+ /// The previous state.
+ /// The next-track action.
+ /// The session.
+ /// The cancellation token.
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, NextTrackGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+
+ ///
+ /// Handles a previous-track action requested by a session. Context's state can change.
+ ///
+ /// The context of the state.
+ /// The previous state.
+ /// The previous-track action.
+ /// The session.
+ /// The cancellation token.
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PreviousTrackGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+
+ ///
+ /// Handles a repeat-mode change requested by a session. Context's state should not change.
+ ///
+ /// The context of the state.
+ /// The previous state.
+ /// The repeat-mode action.
+ /// The session.
+ /// The cancellation token.
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, SetRepeatModeGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+
+ ///
+ /// Handles a shuffle-mode change requested by a session. Context's state should not change.
+ ///
+ /// The context of the state.
+ /// The previous state.
+ /// The shuffle-mode action.
+ /// The session.
+ /// The cancellation token.
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, SetShuffleModeGroupRequest 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 previous state.
/// 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);
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PingGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+
+ ///
+ /// Updates whether the session should be considered during group wait. Context's state should not change.
+ ///
+ /// The context of the state.
+ /// The previous state.
+ /// The ignore-wait action.
+ /// The session.
+ /// The cancellation token.
+ void HandleRequest(ISyncPlayStateContext context, GroupState prevState, IgnoreWaitGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayStateContext.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayStateContext.cs
index 9bdb1ace6c..18a6857491 100644
--- a/MediaBrowser.Controller/SyncPlay/ISyncPlayStateContext.cs
+++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayStateContext.cs
@@ -12,10 +12,34 @@ namespace MediaBrowser.Controller.SyncPlay
public interface ISyncPlayStateContext
{
///
- /// Gets the context's group.
+ /// Gets the default ping value used for sessions, in milliseconds.
///
- /// The group.
- GroupInfo GetGroup();
+ /// The default ping value used for sessions, in milliseconds.
+ long DefaultPing { get; }
+
+ ///
+ /// Gets the group identifier.
+ ///
+ /// The group identifier.
+ Guid GroupId { get; }
+
+ ///
+ /// Gets or sets the position ticks.
+ ///
+ /// The position ticks.
+ long PositionTicks { get; set; }
+
+ ///
+ /// Gets or sets the last activity.
+ ///
+ /// The last activity.
+ DateTime LastActivity { get; set; }
+
+ ///
+ /// Gets the play queue.
+ ///
+ /// The play queue.
+ PlayQueueManager PlayQueue { get; }
///
/// Sets a new state.
@@ -30,7 +54,7 @@ namespace MediaBrowser.Controller.SyncPlay
/// The filtering type.
/// The message to send.
/// The cancellation token.
- /// The task.
+ /// The task.
Task SendGroupUpdate(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate message, CancellationToken cancellationToken);
///
@@ -40,14 +64,14 @@ namespace MediaBrowser.Controller.SyncPlay
/// The filtering type.
/// The message to send.
/// The cancellation token.
- /// The task.
+ /// 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.
+ /// The SendCommand.
SendCommand NewSyncPlayCommand(SendCommandType type);
///
@@ -55,21 +79,135 @@ namespace MediaBrowser.Controller.SyncPlay
///
/// The update type.
/// The data to send.
- /// The GroupUpdate.
+ /// The GroupUpdate.
GroupUpdate NewSyncPlayGroupUpdate(GroupUpdateType type, T data);
///
/// Converts DateTime to UTC string.
///
- /// The date to convert.
- /// The UTC string.
- string DateToUTCString(DateTime date);
+ /// The date to convert.
+ /// The UTC string.
+ string DateToUTCString(DateTime dateTime);
///
/// Sanitizes the PositionTicks, considers the current playing item when available.
///
/// The PositionTicks.
- /// The sanitized PositionTicks.
+ /// The sanitized PositionTicks.
long SanitizePositionTicks(long? positionTicks);
+
+ ///
+ /// Updates the ping of a session, in milliseconds.
+ ///
+ /// The session.
+ /// The ping, in milliseconds.
+ void UpdatePing(SessionInfo session, long ping);
+
+ ///
+ /// Gets the highest ping in the group, in milliseconds.
+ ///
+ /// The highest ping in the group.
+ long GetHighestPing();
+
+ ///
+ /// Sets the session's buffering state.
+ ///
+ /// The session.
+ /// The state.
+ void SetBuffering(SessionInfo session, bool isBuffering);
+
+ ///
+ /// Sets the buffering state of all the sessions.
+ ///
+ /// The state.
+ void SetAllBuffering(bool isBuffering);
+
+ ///
+ /// Gets the group buffering state.
+ ///
+ /// true if there is a session buffering in the group; false otherwise.
+ bool IsBuffering();
+
+ ///
+ /// Sets the session's group wait state.
+ ///
+ /// The session.
+ /// The state.
+ void SetIgnoreGroupWait(SessionInfo session, bool ignoreGroupWait);
+
+ ///
+ /// Sets a new play queue.
+ ///
+ /// The new play queue.
+ /// The playing item position in the play queue.
+ /// The start position ticks.
+ /// true if the play queue has been changed; false is something went wrong.
+ bool SetPlayQueue(Guid[] playQueue, int playingItemPosition, long startPositionTicks);
+
+ ///
+ /// Sets the playing item.
+ ///
+ /// The new playing item id.
+ /// true if the play queue has been changed; false is something went wrong.
+ bool SetPlayingItem(string playlistItemId);
+
+ ///
+ /// Removes items from the play queue.
+ ///
+ /// The items to remove.
+ /// true if playing item got removed; false otherwise.
+ bool RemoveFromPlayQueue(string[] playlistItemIds);
+
+ ///
+ /// Moves an item in the play queue.
+ ///
+ /// The playlist id of the item to move.
+ /// The new position.
+ /// true if item has been moved; false is something went wrong.
+ bool MoveItemInPlayQueue(string playlistItemId, int newIndex);
+
+ ///
+ /// Updates the play queue.
+ ///
+ /// The new items to add to the play queue.
+ /// The mode with which the items will be added.
+ /// true if the play queue has been changed; false is something went wrong.
+ bool AddToPlayQueue(Guid[] newItems, string mode);
+
+ ///
+ /// Restarts current item in play queue.
+ ///
+ void RestartCurrentItem();
+
+ ///
+ /// Picks next item in play queue.
+ ///
+ /// true if the item changed; false otherwise.
+ bool NextItemInQueue();
+
+ ///
+ /// Picks previous item in play queue.
+ ///
+ /// true if the item changed; false otherwise.
+ bool PreviousItemInQueue();
+
+ ///
+ /// Sets the repeat mode.
+ ///
+ /// The new mode.
+ void SetRepeatMode(string mode);
+
+ ///
+ /// Sets the shuffle mode.
+ ///
+ /// The new mode.
+ void SetShuffleMode(string mode);
+
+ ///
+ /// Creates a play queue update.
+ ///
+ /// The reason for the update.
+ /// The play queue update.
+ PlayQueueUpdate GetPlayQueueUpdate(PlayQueueUpdateReason reason);
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/BufferGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/BufferGroupRequest.cs
index 21dae8e4e6..0815dd79b2 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/BufferGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/BufferGroupRequest.cs
@@ -23,21 +23,27 @@ namespace MediaBrowser.Controller.SyncPlay
public long PositionTicks { get; set; }
///
- /// Gets or sets the playing item id.
+ /// Gets or sets the client playback status.
///
- /// The playing item id.
- public Guid PlayingItemId { get; set; }
+ /// The client playback status.
+ public bool IsPlaying { get; set; }
+
+ ///
+ /// Gets or sets the playlist item id of the playing item.
+ ///
+ /// The playlist item id.
+ public string PlaylistItemId { get; set; }
///
- public PlaybackRequestType Type()
+ public PlaybackRequestType GetRequestType()
{
return PlaybackRequestType.Buffer;
}
///
- public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ public void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
{
- return state.HandleRequest(context, false, this, session, cancellationToken);
+ state.HandleRequest(context, state.GetGroupState(), this, session, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/IgnoreWaitGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/IgnoreWaitGroupRequest.cs
new file mode 100644
index 0000000000..5466cbe2f7
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/IgnoreWaitGroupRequest.cs
@@ -0,0 +1,30 @@
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ ///
+ /// Class IgnoreWaitGroupRequest.
+ ///
+ public class IgnoreWaitGroupRequest : IPlaybackGroupRequest
+ {
+ ///
+ /// Gets or sets the client group-wait status.
+ ///
+ /// The client group-wait status.
+ public bool IgnoreWait { get; set; }
+
+ ///
+ public PlaybackRequestType GetRequestType()
+ {
+ return PlaybackRequestType.IgnoreWait;
+ }
+
+ ///
+ public void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(context, state.GetGroupState(), this, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/MovePlaylistItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/MovePlaylistItemGroupRequest.cs
new file mode 100644
index 0000000000..7a293c02fd
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/MovePlaylistItemGroupRequest.cs
@@ -0,0 +1,36 @@
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ ///
+ /// Class MovePlaylistItemGroupRequest.
+ ///
+ public class MovePlaylistItemGroupRequest : IPlaybackGroupRequest
+ {
+ ///
+ /// Gets or sets the playlist id of the item.
+ ///
+ /// The playlist id of the item.
+ public string PlaylistItemId { get; set; }
+
+ ///
+ /// Gets or sets the new position.
+ ///
+ /// The new position.
+ public int NewIndex { get; set; }
+
+ ///
+ public PlaybackRequestType GetRequestType()
+ {
+ return PlaybackRequestType.Queue;
+ }
+
+ ///
+ public void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(context, state.GetGroupState(), this, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/NextTrackGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/NextTrackGroupRequest.cs
new file mode 100644
index 0000000000..d19df2c6a0
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/NextTrackGroupRequest.cs
@@ -0,0 +1,30 @@
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ ///
+ /// Class NextTrackGroupRequest.
+ ///
+ public class NextTrackGroupRequest : IPlaybackGroupRequest
+ {
+ ///
+ /// Gets or sets the playing item id.
+ ///
+ /// The playing item id.
+ public string PlaylistItemId { get; set; }
+
+ ///
+ public PlaybackRequestType GetRequestType()
+ {
+ return PlaybackRequestType.NextTrack;
+ }
+
+ ///
+ public void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(context, state.GetGroupState(), this, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PauseGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PauseGroupRequest.cs
index 21a46add8c..facb25155c 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PauseGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PauseGroupRequest.cs
@@ -10,15 +10,15 @@ namespace MediaBrowser.Controller.SyncPlay
public class PauseGroupRequest : IPlaybackGroupRequest
{
///
- public PlaybackRequestType Type()
+ public PlaybackRequestType GetRequestType()
{
return PlaybackRequestType.Pause;
}
///
- public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ public void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
{
- return state.HandleRequest(context, false, this, session, cancellationToken);
+ state.HandleRequest(context, state.GetGroupState(), this, session, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PingGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PingGroupRequest.cs
index 2f78edfc56..631bf245b1 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PingGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PingGroupRequest.cs
@@ -2,7 +2,6 @@ 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
{
///
@@ -17,15 +16,15 @@ namespace MediaBrowser.Controller.SyncPlay
public long Ping { get; set; }
///
- public PlaybackRequestType Type()
+ public PlaybackRequestType GetRequestType()
{
return PlaybackRequestType.Ping;
}
///
- public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ public void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
{
- return state.HandleRequest(context, false, this, session, cancellationToken);
+ state.HandleRequest(context, state.GetGroupState(), this, session, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PlayGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PlayGroupRequest.cs
index 942229a775..f3dd769e46 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PlayGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PlayGroupRequest.cs
@@ -1,3 +1,4 @@
+using System;
using System.Threading;
using MediaBrowser.Model.SyncPlay;
using MediaBrowser.Controller.Session;
@@ -9,16 +10,34 @@ namespace MediaBrowser.Controller.SyncPlay
///
public class PlayGroupRequest : IPlaybackGroupRequest
{
+ ///
+ /// Gets or sets the playing queue.
+ ///
+ /// The playing queue.
+ public Guid[] PlayingQueue { get; set; }
+
+ ///
+ /// Gets or sets the playing item from the queue.
+ ///
+ /// The playing item.
+ public int PlayingItemPosition { get; set; }
+
+ ///
+ /// Gets or sets the start position ticks.
+ ///
+ /// The start position ticks.
+ public long StartPositionTicks { get; set; }
+
///
- public PlaybackRequestType Type()
+ public PlaybackRequestType GetRequestType()
{
return PlaybackRequestType.Play;
}
///
- public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ public void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
{
- return state.HandleRequest(context, false, this, session, cancellationToken);
+ state.HandleRequest(context, state.GetGroupState(), this, session, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PreviousTrackGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PreviousTrackGroupRequest.cs
new file mode 100644
index 0000000000..663011b429
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PreviousTrackGroupRequest.cs
@@ -0,0 +1,30 @@
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ ///
+ /// Class PreviousTrackGroupRequest.
+ ///
+ public class PreviousTrackGroupRequest : IPlaybackGroupRequest
+ {
+ ///
+ /// Gets or sets the playing item id.
+ ///
+ /// The playing item id.
+ public string PlaylistItemId { get; set; }
+
+ ///
+ public PlaybackRequestType GetRequestType()
+ {
+ return PlaybackRequestType.PreviousTrack;
+ }
+
+ ///
+ public void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(context, state.GetGroupState(), this, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/QueueGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/QueueGroupRequest.cs
new file mode 100644
index 0000000000..01c08cc860
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/QueueGroupRequest.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ ///
+ /// Class QueueGroupRequest.
+ ///
+ public class QueueGroupRequest : IPlaybackGroupRequest
+ {
+ ///
+ /// Gets or sets the items to queue.
+ ///
+ /// The items to queue.
+ public Guid[] ItemIds { get; set; }
+
+ ///
+ /// Gets or sets the mode in which to add the new items.
+ ///
+ /// The mode.
+ public string Mode { get; set; }
+
+ ///
+ public PlaybackRequestType GetRequestType()
+ {
+ return PlaybackRequestType.Queue;
+ }
+
+ ///
+ public void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(context, state.GetGroupState(), this, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/ReadyGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/ReadyGroupRequest.cs
index ee88ddddbb..16bc67c617 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/ReadyGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/ReadyGroupRequest.cs
@@ -23,21 +23,27 @@ namespace MediaBrowser.Controller.SyncPlay
public long PositionTicks { get; set; }
///
- /// Gets or sets the playing item id.
+ /// Gets or sets the client playback status.
///
- /// The playing item id.
- public Guid PlayingItemId { get; set; }
+ /// The client playback status.
+ public bool IsPlaying { get; set; }
+
+ ///
+ /// Gets or sets the playlist item id of the playing item.
+ ///
+ /// The playlist item id.
+ public string PlaylistItemId { get; set; }
///
- public PlaybackRequestType Type()
+ public PlaybackRequestType GetRequestType()
{
return PlaybackRequestType.Ready;
}
///
- public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ public void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
{
- return state.HandleRequest(context, false, this, session, cancellationToken);
+ state.HandleRequest(context, state.GetGroupState(), this, session, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/RemoveFromPlaylistGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/RemoveFromPlaylistGroupRequest.cs
new file mode 100644
index 0000000000..3fc77f6771
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/RemoveFromPlaylistGroupRequest.cs
@@ -0,0 +1,30 @@
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ ///
+ /// Class RemoveFromPlaylistGroupRequest.
+ ///
+ public class RemoveFromPlaylistGroupRequest : IPlaybackGroupRequest
+ {
+ ///
+ /// Gets or sets the playlist ids ot the items.
+ ///
+ /// The playlist ids ot the items.
+ public string[] PlaylistItemIds { get; set; }
+
+ ///
+ public PlaybackRequestType GetRequestType()
+ {
+ return PlaybackRequestType.Queue;
+ }
+
+ ///
+ public void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(context, state.GetGroupState(), this, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SeekGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SeekGroupRequest.cs
index bb5e7a343e..24d9be5073 100644
--- a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SeekGroupRequest.cs
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SeekGroupRequest.cs
@@ -16,15 +16,15 @@ namespace MediaBrowser.Controller.SyncPlay
public long PositionTicks { get; set; }
///
- public PlaybackRequestType Type()
+ public PlaybackRequestType GetRequestType()
{
return PlaybackRequestType.Seek;
}
///
- public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ public void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
{
- return state.HandleRequest(context, false, this, session, cancellationToken);
+ state.HandleRequest(context, state.GetGroupState(), this, session, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SetCurrentItemGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SetCurrentItemGroupRequest.cs
new file mode 100644
index 0000000000..d70559899a
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SetCurrentItemGroupRequest.cs
@@ -0,0 +1,30 @@
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ ///
+ /// Class SetPlaylistItemGroupRequest.
+ ///
+ public class SetPlaylistItemGroupRequest : IPlaybackGroupRequest
+ {
+ ///
+ /// Gets or sets the playlist id of the playing item.
+ ///
+ /// The playlist id of the playing item.
+ public string PlaylistItemId { get; set; }
+
+ ///
+ public PlaybackRequestType GetRequestType()
+ {
+ return PlaybackRequestType.SetPlaylistItem;
+ }
+
+ ///
+ public void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(context, state.GetGroupState(), this, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SetRepeatModeGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SetRepeatModeGroupRequest.cs
new file mode 100644
index 0000000000..5f36f60e45
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SetRepeatModeGroupRequest.cs
@@ -0,0 +1,30 @@
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ ///
+ /// Class SetRepeatModeGroupRequest.
+ ///
+ public class SetRepeatModeGroupRequest : IPlaybackGroupRequest
+ {
+ ///
+ /// Gets or sets the repeat mode.
+ ///
+ /// The repeat mode.
+ public string Mode { get; set; }
+
+ ///
+ public PlaybackRequestType GetRequestType()
+ {
+ return PlaybackRequestType.SetRepeatMode;
+ }
+
+ ///
+ public void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(context, state.GetGroupState(), this, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SetShuffleModeGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SetShuffleModeGroupRequest.cs
new file mode 100644
index 0000000000..472455fd3b
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SetShuffleModeGroupRequest.cs
@@ -0,0 +1,30 @@
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ ///
+ /// Class SetShuffleModeGroupRequest.
+ ///
+ public class SetShuffleModeGroupRequest : IPlaybackGroupRequest
+ {
+ ///
+ /// Gets or sets the shuffle mode.
+ ///
+ /// The shuffle mode.
+ public string Mode { get; set; }
+
+ ///
+ public PlaybackRequestType GetRequestType()
+ {
+ return PlaybackRequestType.SetShuffleMode;
+ }
+
+ ///
+ public void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(context, state.GetGroupState(), this, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/StopGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/StopGroupRequest.cs
new file mode 100644
index 0000000000..f1581c98d9
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/StopGroupRequest.cs
@@ -0,0 +1,24 @@
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ ///
+ /// Class StopGroupRequest.
+ ///
+ public class StopGroupRequest : IPlaybackGroupRequest
+ {
+ ///
+ public PlaybackRequestType GetRequestType()
+ {
+ return PlaybackRequestType.Stop;
+ }
+
+ ///
+ public void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(context, state.GetGroupState(), this, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/UnpauseGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/UnpauseGroupRequest.cs
new file mode 100644
index 0000000000..1072952081
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/UnpauseGroupRequest.cs
@@ -0,0 +1,24 @@
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ ///
+ /// Class UnpauseGroupRequest.
+ ///
+ public class UnpauseGroupRequest : IPlaybackGroupRequest
+ {
+ ///
+ public PlaybackRequestType GetRequestType()
+ {
+ return PlaybackRequestType.Unpause;
+ }
+
+ ///
+ public void Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ state.HandleRequest(context, state.GetGroupState(), this, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
new file mode 100644
index 0000000000..701982cc00
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
@@ -0,0 +1,596 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ static class ListShuffleExtension
+ {
+ private static Random rng = new Random();
+ public static void Shuffle(this IList list)
+ {
+ int n = list.Count;
+ while (n > 1)
+ {
+ n--;
+ int k = rng.Next(n + 1);
+ T value = list[k];
+ list[k] = list[n];
+ list[n] = value;
+ }
+ }
+ }
+
+ ///
+ /// Class PlayQueueManager.
+ ///
+ public class PlayQueueManager : IDisposable
+ {
+ ///
+ /// Gets or sets the playing item index.
+ ///
+ /// The playing item index.
+ public int PlayingItemIndex { get; private set; }
+
+ ///
+ /// Gets or sets the last time the queue has been changed.
+ ///
+ /// The last time the queue has been changed.
+ public DateTime LastChange { get; private set; }
+
+ ///
+ /// Gets the sorted playlist.
+ ///
+ /// The sorted playlist, or play queue of the group.
+ private List SortedPlaylist { get; set; } = new List();
+
+ ///
+ /// Gets the shuffled playlist.
+ ///
+ /// The shuffled playlist, or play queue of the group.
+ private List ShuffledPlaylist { get; set; } = new List();
+
+ ///
+ /// Gets or sets the shuffle mode.
+ ///
+ /// The shuffle mode.
+ public GroupShuffleMode ShuffleMode { get; private set; } = GroupShuffleMode.Sorted;
+
+ ///
+ /// Gets or sets the repeat mode.
+ ///
+ /// The repeat mode.
+ public GroupRepeatMode RepeatMode { get; private set; } = GroupRepeatMode.RepeatNone;
+
+ ///
+ /// Gets or sets the progressive id counter.
+ ///
+ /// The progressive id.
+ private int ProgressiveId { get; set; } = 0;
+
+ private bool _disposed = false;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PlayQueueManager()
+ {
+ Reset();
+ }
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Releases unmanaged and optionally managed resources.
+ ///
+ /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+ }
+
+ ///
+ /// Gets the next available id.
+ ///
+ /// The next available id.
+ private int GetNextProgressiveId() {
+ return ProgressiveId++;
+ }
+
+ ///
+ /// Creates a list from the array of items. Each item is given an unique playlist id.
+ ///
+ /// The list of queue items.
+ private List CreateQueueItemsFromArray(Guid[] items)
+ {
+ return items.ToList()
+ .Select(item => new QueueItem()
+ {
+ ItemId = item,
+ PlaylistItemId = "syncPlayItem" + GetNextProgressiveId()
+ })
+ .ToList();
+ }
+
+ ///
+ /// Gets the current playlist, depending on the shuffle mode.
+ ///
+ /// The playlist.
+ private List GetPlaylistAsList()
+ {
+ if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
+ {
+ return ShuffledPlaylist;
+ }
+ else
+ {
+ return SortedPlaylist;
+ }
+ }
+
+ ///
+ /// Gets the current playlist as an array, depending on the shuffle mode.
+ ///
+ /// The array of items in the playlist.
+ public QueueItem[] GetPlaylist() {
+ if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
+ {
+ return ShuffledPlaylist.ToArray();
+ }
+ else
+ {
+ return SortedPlaylist.ToArray();
+ }
+ }
+
+ ///
+ /// Sets a new playlist. Playing item is set to none. Resets shuffle mode and repeat mode as well.
+ ///
+ /// The new items of the playlist.
+ public void SetPlaylist(Guid[] items)
+ {
+ SortedPlaylist = CreateQueueItemsFromArray(items);
+ PlayingItemIndex = -1;
+ ShuffleMode = GroupShuffleMode.Sorted;
+ RepeatMode = GroupRepeatMode.RepeatNone;
+ LastChange = DateTime.UtcNow;
+ }
+
+ ///
+ /// Appends new items to the playlist. The specified order is mantained for the sorted playlist, whereas items get shuffled for the shuffled playlist.
+ ///
+ /// The items to add to the playlist.
+ public void Queue(Guid[] items)
+ {
+ var newItems = CreateQueueItemsFromArray(items);
+ SortedPlaylist.AddRange(newItems);
+
+ if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
+ {
+ newItems.Shuffle();
+ ShuffledPlaylist.AddRange(newItems);
+ }
+
+ LastChange = DateTime.UtcNow;
+ }
+
+ ///
+ /// Shuffles the playlist. Shuffle mode is changed.
+ ///
+ public void ShufflePlaylist()
+ {
+ if (SortedPlaylist.Count() == 0)
+ {
+ return;
+ }
+
+ if (PlayingItemIndex < 0) {
+ ShuffledPlaylist = SortedPlaylist.ToList();
+ ShuffledPlaylist.Shuffle();
+ }
+ else
+ {
+ var playingItem = SortedPlaylist[PlayingItemIndex];
+ ShuffledPlaylist = SortedPlaylist.ToList();
+ ShuffledPlaylist.RemoveAt(PlayingItemIndex);
+ ShuffledPlaylist.Shuffle();
+ ShuffledPlaylist = ShuffledPlaylist.Prepend(playingItem).ToList();
+ PlayingItemIndex = 0;
+ }
+
+ ShuffleMode = GroupShuffleMode.Shuffle;
+ LastChange = DateTime.UtcNow;
+ }
+
+ ///
+ /// Resets the playlist to sorted mode. Shuffle mode is changed.
+ ///
+ public void SortShuffledPlaylist()
+ {
+ if (PlayingItemIndex >= 0)
+ {
+ var playingItem = ShuffledPlaylist[PlayingItemIndex];
+ PlayingItemIndex = SortedPlaylist.IndexOf(playingItem);
+ }
+
+ ShuffledPlaylist.Clear();
+
+ ShuffleMode = GroupShuffleMode.Sorted;
+ LastChange = DateTime.UtcNow;
+ }
+
+ ///
+ /// Clears the playlist.
+ ///
+ /// Whether to remove the playing item as well.
+ public void ClearPlaylist(bool clearPlayingItem)
+ {
+ var playingItem = SortedPlaylist[PlayingItemIndex];
+ SortedPlaylist.Clear();
+ ShuffledPlaylist.Clear();
+ LastChange = DateTime.UtcNow;
+
+ if (!clearPlayingItem && playingItem != null)
+ {
+ SortedPlaylist.Add(playingItem);
+ if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
+ {
+ SortedPlaylist.Add(playingItem);
+ }
+ }
+ }
+
+ ///
+ /// Adds new items to the playlist right after the playing item. The specified order is mantained for the sorted playlist, whereas items get shuffled for the shuffled playlist.
+ ///
+ /// The items to add to the playlist.
+ public void QueueNext(Guid[] items)
+ {
+ var newItems = CreateQueueItemsFromArray(items);
+
+ if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
+ {
+ // Append items to sorted playlist as they are
+ SortedPlaylist.AddRange(newItems);
+ // Shuffle items before adding to shuffled playlist
+ newItems.Shuffle();
+ ShuffledPlaylist.InsertRange(PlayingItemIndex + 1, newItems);
+ }
+ else
+ {
+ SortedPlaylist.InsertRange(PlayingItemIndex + 1, newItems);
+ }
+
+ LastChange = DateTime.UtcNow;
+ }
+
+ ///
+ /// Gets playlist id of the playing item, if any.
+ ///
+ /// The playlist id of the playing item.
+ public string GetPlayingItemPlaylistId()
+ {
+ if (PlayingItemIndex < 0)
+ {
+ return null;
+ }
+
+ var list = GetPlaylistAsList();
+
+ if (list.Count() > 0)
+ {
+ return list[PlayingItemIndex].PlaylistItemId;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Gets the playing item id, if any.
+ ///
+ /// The playing item id.
+ public Guid GetPlayingItemId()
+ {
+ if (PlayingItemIndex < 0)
+ {
+ return Guid.Empty;
+ }
+
+ var list = GetPlaylistAsList();
+
+ if (list.Count() > 0)
+ {
+ return list[PlayingItemIndex].ItemId;
+ }
+ else
+ {
+ return Guid.Empty;
+ }
+ }
+
+ ///
+ /// Sets the playing item using its id. If not in the playlist, the playing item is reset.
+ ///
+ /// The new playing item id.
+ public void SetPlayingItemById(Guid itemId)
+ {
+ var itemIds = GetPlaylistAsList().Select(queueItem => queueItem.ItemId).ToList();
+ PlayingItemIndex = itemIds.IndexOf(itemId);
+ LastChange = DateTime.UtcNow;
+ }
+
+ ///
+ /// Sets the playing item using its playlist id. If not in the playlist, the playing item is reset.
+ ///
+ /// The new playing item id.
+ /// true if playing item has been set; false if item is not in the playlist.
+ public bool SetPlayingItemByPlaylistId(string playlistItemId)
+ {
+ var playlistIds = GetPlaylistAsList().Select(queueItem => queueItem.PlaylistItemId).ToList();
+ PlayingItemIndex = playlistIds.IndexOf(playlistItemId);
+ LastChange = DateTime.UtcNow;
+ return PlayingItemIndex != -1;
+ }
+
+ ///
+ /// Sets the playing item using its position. If not in range, the playing item is reset.
+ ///
+ /// The new playing item index.
+ public void SetPlayingItemByIndex(int playlistIndex)
+ {
+ var list = GetPlaylistAsList();
+ if (playlistIndex < 0 || playlistIndex > list.Count())
+ {
+ PlayingItemIndex = -1;
+ }
+ else
+ {
+ PlayingItemIndex = playlistIndex;
+ }
+
+ LastChange = DateTime.UtcNow;
+ }
+
+ ///
+ /// Removes items from the playlist. If not removed, the playing item is preserved.
+ ///
+ /// The items to remove.
+ /// true if playing item got removed; false otherwise.
+ public bool RemoveFromPlaylist(string[] playlistItemIds)
+ {
+ var playingItem = SortedPlaylist[PlayingItemIndex];
+ if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
+ {
+ playingItem = ShuffledPlaylist[PlayingItemIndex];
+ }
+
+ var playlistItemIdsList = playlistItemIds.ToList();
+ SortedPlaylist.RemoveAll(item => playlistItemIdsList.Contains(item.PlaylistItemId));
+ ShuffledPlaylist.RemoveAll(item => playlistItemIdsList.Contains(item.PlaylistItemId));
+
+ LastChange = DateTime.UtcNow;
+
+ if (playingItem != null)
+ {
+ if (playlistItemIds.Contains(playingItem.PlaylistItemId))
+ {
+ // Playing item has been removed, picking previous item
+ PlayingItemIndex--;
+ if (PlayingItemIndex < 0)
+ {
+ // Was first element, picking next if available
+ PlayingItemIndex = SortedPlaylist.Count() > 0 ? 0 : -1;
+ }
+
+ return true;
+ }
+ else
+ {
+ // Restoring playing item
+ SetPlayingItemByPlaylistId(playingItem.PlaylistItemId);
+ return false;
+ }
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Moves an item in the playlist to another position.
+ ///
+ /// The item to move.
+ /// The new position.
+ /// true if the item has been moved; false otherwise.
+ public bool MovePlaylistItem(string playlistItemId, int newIndex)
+ {
+ var list = GetPlaylistAsList();
+ var playingItem = list[PlayingItemIndex];
+
+ var playlistIds = list.Select(queueItem => queueItem.PlaylistItemId).ToList();
+ var oldIndex = playlistIds.IndexOf(playlistItemId);
+ if (oldIndex < 0) {
+ return false;
+ }
+
+ var queueItem = list[oldIndex];
+ list.RemoveAt(oldIndex);
+ newIndex = newIndex > list.Count() ? list.Count() : newIndex;
+ newIndex = newIndex < 0 ? 0 : newIndex;
+ list.Insert(newIndex, queueItem);
+
+ LastChange = DateTime.UtcNow;
+ PlayingItemIndex = list.IndexOf(playingItem);
+ return true;
+ }
+
+ ///
+ /// Resets the playlist to its initial state.
+ ///
+ public void Reset()
+ {
+ ProgressiveId = 0;
+ SortedPlaylist.Clear();
+ ShuffledPlaylist.Clear();
+ PlayingItemIndex = -1;
+ ShuffleMode = GroupShuffleMode.Sorted;
+ RepeatMode = GroupRepeatMode.RepeatNone;
+ LastChange = DateTime.UtcNow;
+ }
+
+ ///
+ /// Sets the repeat mode.
+ ///
+ /// The new mode.
+ public void SetRepeatMode(string mode)
+ {
+ switch (mode)
+ {
+ case "RepeatOne":
+ RepeatMode = GroupRepeatMode.RepeatOne;
+ break;
+ case "RepeatAll":
+ RepeatMode = GroupRepeatMode.RepeatAll;
+ break;
+ default:
+ RepeatMode = GroupRepeatMode.RepeatNone;
+ break;
+ }
+
+ LastChange = DateTime.UtcNow;
+ }
+
+ ///
+ /// Sets the shuffle mode.
+ ///
+ /// The new mode.
+ public void SetShuffleMode(string mode)
+ {
+ switch (mode)
+ {
+ case "Shuffle":
+ ShufflePlaylist();
+ break;
+ default:
+ SortShuffledPlaylist();
+ break;
+ }
+ }
+
+ ///
+ /// Toggles the shuffle mode between sorted and shuffled.
+ ///
+ public void ToggleShuffleMode()
+ {
+ SetShuffleMode(ShuffleMode.Equals(GroupShuffleMode.Shuffle) ? "Shuffle" : "");
+ }
+
+ ///
+ /// Gets the next item in the playlist considering repeat mode and shuffle mode.
+ ///
+ /// The next item in the playlist.
+ public QueueItem GetNextItemPlaylistId()
+ {
+ int newIndex;
+ var playlist = GetPlaylistAsList();
+
+ switch (RepeatMode)
+ {
+ case GroupRepeatMode.RepeatOne:
+ newIndex = PlayingItemIndex;
+ break;
+ case GroupRepeatMode.RepeatAll:
+ newIndex = PlayingItemIndex + 1;
+ if (newIndex >= playlist.Count())
+ {
+ newIndex = 0;
+ }
+ break;
+ default:
+ newIndex = PlayingItemIndex + 1;
+ break;
+ }
+
+ if (newIndex < 0 || newIndex >= playlist.Count())
+ {
+ return null;
+ }
+
+ return playlist[newIndex];
+ }
+
+ ///
+ /// Sets the next item in the queue as playing item.
+ ///
+ /// true if the playing item changed; false otherwise.
+ public bool Next()
+ {
+ if (RepeatMode.Equals(GroupRepeatMode.RepeatOne))
+ {
+ LastChange = DateTime.UtcNow;
+ return true;
+ }
+
+ PlayingItemIndex++;
+ if (PlayingItemIndex >= SortedPlaylist.Count())
+ {
+ if (RepeatMode.Equals(GroupRepeatMode.RepeatAll))
+ {
+ PlayingItemIndex = 0;
+ }
+ else
+ {
+ PlayingItemIndex--;
+ return false;
+ }
+ }
+
+ LastChange = DateTime.UtcNow;
+ return true;
+ }
+
+ ///
+ /// Sets the previous item in the queue as playing item.
+ ///
+ /// true if the playing item changed; false otherwise.
+ public bool Previous()
+ {
+ if (RepeatMode.Equals(GroupRepeatMode.RepeatOne))
+ {
+ LastChange = DateTime.UtcNow;
+ return true;
+ }
+
+ PlayingItemIndex--;
+ if (PlayingItemIndex < 0)
+ {
+ if (RepeatMode.Equals(GroupRepeatMode.RepeatAll))
+ {
+ PlayingItemIndex = SortedPlaylist.Count() - 1;
+ }
+ else
+ {
+ PlayingItemIndex++;
+ return false;
+ }
+ }
+
+ LastChange = DateTime.UtcNow;
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/SyncPlayAbstractState.cs b/MediaBrowser.Controller/SyncPlay/SyncPlayAbstractState.cs
deleted file mode 100644
index 0b72d16686..0000000000
--- a/MediaBrowser.Controller/SyncPlay/SyncPlayAbstractState.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-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/GroupInfoDto.cs b/MediaBrowser.Model/SyncPlay/GroupInfoDto.cs
index ac84a26dc3..255f6812bd 100644
--- a/MediaBrowser.Model/SyncPlay/GroupInfoDto.cs
+++ b/MediaBrowser.Model/SyncPlay/GroupInfoDto.cs
@@ -5,7 +5,7 @@ using System.Collections.Generic;
namespace MediaBrowser.Model.SyncPlay
{
///
- /// Class GroupInfoView.
+ /// Class GroupInfoDto.
///
public class GroupInfoDto
{
@@ -16,27 +16,27 @@ namespace MediaBrowser.Model.SyncPlay
public string GroupId { get; set; }
///
- /// Gets or sets the playing item id.
+ /// Gets or sets the group name.
///
- /// The playing item id.
- public string PlayingItemId { get; set; }
+ /// The group name.
+ public string GroupName { get; set; }
///
- /// Gets or sets the playing item name.
+ /// Gets or sets the group state.
///
- /// The playing item name.
- public string PlayingItemName { get; set; }
-
- ///
- /// Gets or sets the position ticks.
- ///
- /// The position ticks.
- public long PositionTicks { get; set; }
+ /// The group state.
+ public GroupState State { get; set; }
///
/// Gets or sets the participants.
///
/// The participants.
public IReadOnlyList Participants { get; set; }
+
+ ///
+ /// Gets or sets the date when this dto has been updated.
+ ///
+ /// The date when this dto has been updated.
+ public string LastUpdatedAt { get; set; }
}
}
diff --git a/MediaBrowser.Model/SyncPlay/GroupRepeatMode.cs b/MediaBrowser.Model/SyncPlay/GroupRepeatMode.cs
new file mode 100644
index 0000000000..4895e57b7f
--- /dev/null
+++ b/MediaBrowser.Model/SyncPlay/GroupRepeatMode.cs
@@ -0,0 +1,23 @@
+namespace MediaBrowser.Model.SyncPlay
+{
+ ///
+ /// Enum GroupRepeatMode.
+ ///
+ public enum GroupRepeatMode
+ {
+ ///
+ /// Repeat one item only.
+ ///
+ RepeatOne = 0,
+
+ ///
+ /// Cycle the playlist.
+ ///
+ RepeatAll = 1,
+
+ ///
+ /// Do not repeat.
+ ///
+ RepeatNone = 2
+ }
+}
diff --git a/MediaBrowser.Model/SyncPlay/GroupShuffleMode.cs b/MediaBrowser.Model/SyncPlay/GroupShuffleMode.cs
new file mode 100644
index 0000000000..de860883c0
--- /dev/null
+++ b/MediaBrowser.Model/SyncPlay/GroupShuffleMode.cs
@@ -0,0 +1,18 @@
+namespace MediaBrowser.Model.SyncPlay
+{
+ ///
+ /// Enum GroupShuffleMode.
+ ///
+ public enum GroupShuffleMode
+ {
+ ///
+ /// Sorted playlist.
+ ///
+ Sorted = 0,
+
+ ///
+ /// Shuffled playlist.
+ ///
+ Shuffle = 1
+ }
+}
diff --git a/MediaBrowser.Model/SyncPlay/GroupStateUpdate.cs b/MediaBrowser.Model/SyncPlay/GroupStateUpdate.cs
new file mode 100644
index 0000000000..7c7b267e6f
--- /dev/null
+++ b/MediaBrowser.Model/SyncPlay/GroupStateUpdate.cs
@@ -0,0 +1,22 @@
+#nullable disable
+
+namespace MediaBrowser.Model.SyncPlay
+{
+ ///
+ /// Class GroupStateUpdate.
+ ///
+ public class GroupStateUpdate
+ {
+ ///
+ /// Gets or sets the state of the group.
+ ///
+ /// The state of the group.
+ public GroupState State { get; set; }
+
+ ///
+ /// Gets or sets the reason of the state change.
+ ///
+ /// The reason of the state change.
+ public PlaybackRequestType Reason { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/SyncPlay/GroupUpdateType.cs b/MediaBrowser.Model/SyncPlay/GroupUpdateType.cs
index c749f7b13a..7423bff117 100644
--- a/MediaBrowser.Model/SyncPlay/GroupUpdateType.cs
+++ b/MediaBrowser.Model/SyncPlay/GroupUpdateType.cs
@@ -26,14 +26,14 @@ namespace MediaBrowser.Model.SyncPlay
GroupLeft,
///
- /// The group-wait update. Tells members of the group that a user is buffering.
+ /// The group-state update. Tells members of the group that the state changed.
///
- GroupWait,
+ StateUpdate,
///
- /// The prepare-session update. Tells a user to load some content.
+ /// The play-queue update. Tells a user what's the playing queue of the group.
///
- PrepareSession,
+ PlayQueue,
///
/// The not-in-group error. Tells a user that they don't belong to a group.
diff --git a/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs b/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs
index 0c77a61322..04f3a73b17 100644
--- a/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs
+++ b/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs
@@ -8,9 +8,9 @@ namespace MediaBrowser.Model.SyncPlay
public class JoinGroupRequest
{
///
- /// Gets or sets the Group id.
+ /// Gets or sets the group id.
///
- /// The Group id to join.
+ /// The id of the group to join.
public Guid GroupId { get; set; }
}
}
diff --git a/MediaBrowser.Model/SyncPlay/NewGroupRequest.cs b/MediaBrowser.Model/SyncPlay/NewGroupRequest.cs
new file mode 100644
index 0000000000..ccab5313f7
--- /dev/null
+++ b/MediaBrowser.Model/SyncPlay/NewGroupRequest.cs
@@ -0,0 +1,16 @@
+#nullable disable
+
+namespace MediaBrowser.Model.SyncPlay
+{
+ ///
+ /// Class NewGroupRequest.
+ ///
+ public class NewGroupRequest
+ {
+ ///
+ /// Gets or sets the group name.
+ ///
+ /// The name of the new group.
+ public string GroupName { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/SyncPlay/PlayQueueUpdate.cs b/MediaBrowser.Model/SyncPlay/PlayQueueUpdate.cs
new file mode 100644
index 0000000000..5e2740a892
--- /dev/null
+++ b/MediaBrowser.Model/SyncPlay/PlayQueueUpdate.cs
@@ -0,0 +1,52 @@
+#nullable disable
+
+namespace MediaBrowser.Model.SyncPlay
+{
+ ///
+ /// Class PlayQueueUpdate.
+ ///
+ public class PlayQueueUpdate
+ {
+ ///
+ /// Gets or sets the request type that originated this update.
+ ///
+ /// The reason for the update.
+ public PlayQueueUpdateReason Reason { get; set; }
+
+ ///
+ /// Gets or sets the UTC time of the last change to the playing queue.
+ ///
+ /// The UTC time of the last change to the playing queue.
+ public string LastUpdate { get; set; }
+
+ ///
+ /// Gets or sets the playlist.
+ ///
+ /// The playlist.
+ public QueueItem[] Playlist { get; set; }
+
+ ///
+ /// Gets or sets the playing item index in the playlist.
+ ///
+ /// The playing item index in the playlist.
+ public int PlayingItemIndex { get; set; }
+
+ ///
+ /// Gets or sets the start position ticks.
+ ///
+ /// The start position ticks.
+ public long StartPositionTicks { get; set; }
+
+ ///
+ /// Gets or sets the shuffle mode.
+ ///
+ /// The shuffle mode.
+ public GroupShuffleMode ShuffleMode { get; set; }
+
+ ///
+ /// Gets or sets the repeat mode.
+ ///
+ /// The repeat mode.
+ public GroupRepeatMode RepeatMode { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/SyncPlay/PlayQueueUpdateReason.cs b/MediaBrowser.Model/SyncPlay/PlayQueueUpdateReason.cs
new file mode 100644
index 0000000000..4b3f6eb4d6
--- /dev/null
+++ b/MediaBrowser.Model/SyncPlay/PlayQueueUpdateReason.cs
@@ -0,0 +1,58 @@
+namespace MediaBrowser.Model.SyncPlay
+{
+ ///
+ /// Enum PlayQueueUpdateReason.
+ ///
+ public enum PlayQueueUpdateReason
+ {
+ ///
+ /// A user is requesting to play a new playlist.
+ ///
+ NewPlaylist = 0,
+
+ ///
+ /// A user is changing the playing item.
+ ///
+ SetCurrentItem = 1,
+
+ ///
+ /// A user is removing items from the playlist.
+ ///
+ RemoveItems = 2,
+
+ ///
+ /// A user is moving an item in the playlist.
+ ///
+ MoveItem = 3,
+
+ ///
+ /// A user is making changes to the queue.
+ ///
+ Queue = 4,
+
+ ///
+ /// A user is making changes to the queue.
+ ///
+ QueueNext = 5,
+
+ ///
+ /// A user is requesting the next item in queue.
+ ///
+ NextTrack = 6,
+
+ ///
+ /// A user is requesting the previous item in queue.
+ ///
+ PreviousTrack = 7,
+
+ ///
+ /// A user is changing repeat mode.
+ ///
+ RepeatMode = 8,
+
+ ///
+ /// A user is changing shuffle mode.
+ ///
+ ShuffleMode = 9
+ }
+}
diff --git a/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs
index e89efeed8a..0d0f48ea97 100644
--- a/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs
+++ b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs
@@ -6,33 +6,87 @@ namespace MediaBrowser.Model.SyncPlay
public enum PlaybackRequestType
{
///
- /// A user is requesting a play command for the group.
+ /// A user is setting a new playlist.
///
Play = 0,
+ ///
+ /// A user is changing the playlist item.
+ ///
+ SetPlaylistItem = 1,
+
+ ///
+ /// A user is removing items from the playlist.
+ ///
+ RemoveFromPlaylist = 2,
+
+ ///
+ /// A user is moving an item in the playlist.
+ ///
+ MovePlaylistItem = 3,
+
+ ///
+ /// A user is adding items to the playlist.
+ ///
+ Queue = 4,
+
+ ///
+ /// A user is requesting an unpause command for the group.
+ ///
+ Unpause = 5,
+
///
/// A user is requesting a pause command for the group.
///
- Pause = 1,
+ Pause = 6,
+
+ ///
+ /// A user is requesting a stop command for the group.
+ ///
+ Stop = 7,
///
/// A user is requesting a seek command for the group.
///
- Seek = 2,
+ Seek = 8,
- ///
+ ///
/// A user is signaling that playback is buffering.
///
- Buffer = 3,
+ Buffer = 9,
///
/// A user is signaling that playback resumed.
///
- Ready = 4,
+ Ready = 10,
///
- /// A user is reporting its ping.
+ /// A user is requesting next track in playlist.
///
- Ping = 5
+ NextTrack = 11,
+
+ ///
+ /// A user is requesting previous track in playlist.
+ ///
+ PreviousTrack = 12,
+ ///
+ /// A user is setting the repeat mode.
+ ///
+ SetRepeatMode = 13,
+
+ ///
+ /// A user is setting the shuffle mode.
+ ///
+ SetShuffleMode = 14,
+
+ ///
+ /// A user is reporting their ping.
+ ///
+ Ping = 15,
+
+ ///
+ /// A user is requesting to be ignored on group wait.
+ ///
+ IgnoreWait = 16
}
}
diff --git a/MediaBrowser.Model/SyncPlay/QueueItem.cs b/MediaBrowser.Model/SyncPlay/QueueItem.cs
new file mode 100644
index 0000000000..dc9cfbc229
--- /dev/null
+++ b/MediaBrowser.Model/SyncPlay/QueueItem.cs
@@ -0,0 +1,24 @@
+#nullable disable
+
+using System;
+
+namespace MediaBrowser.Model.SyncPlay
+{
+ ///
+ /// Class QueueItem.
+ ///
+ public class QueueItem
+ {
+ ///
+ /// Gets or sets the item id.
+ ///
+ /// The item id.
+ public Guid ItemId { get; set; }
+
+ ///
+ /// Gets or sets the playlist id of the item.
+ ///
+ /// The playlist id of the item.
+ public string PlaylistItemId { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/SyncPlay/SendCommand.cs b/MediaBrowser.Model/SyncPlay/SendCommand.cs
index 0f0be0152d..779f711af0 100644
--- a/MediaBrowser.Model/SyncPlay/SendCommand.cs
+++ b/MediaBrowser.Model/SyncPlay/SendCommand.cs
@@ -13,6 +13,12 @@ namespace MediaBrowser.Model.SyncPlay
/// The group identifier.
public string GroupId { get; set; }
+ ///
+ /// Gets or sets the playlist id of the playing item.
+ ///
+ /// The playlist id of the playing item.
+ public string PlaylistItemId { get; set; }
+
///
/// Gets or sets the UTC time when to execute the command.
///
diff --git a/MediaBrowser.Model/SyncPlay/SendCommandType.cs b/MediaBrowser.Model/SyncPlay/SendCommandType.cs
index 86dec9e900..e6b17c60ae 100644
--- a/MediaBrowser.Model/SyncPlay/SendCommandType.cs
+++ b/MediaBrowser.Model/SyncPlay/SendCommandType.cs
@@ -6,18 +6,23 @@ namespace MediaBrowser.Model.SyncPlay
public enum SendCommandType
{
///
- /// The play command. Instructs users to start playback.
+ /// The unpause command. Instructs users to unpause playback.
///
- Play = 0,
+ Unpause = 0,
///
/// The pause command. Instructs users to pause playback.
///
Pause = 1,
+ ///
+ /// The stop command. Instructs users to stop playback.
+ ///
+ Stop = 2,
+
///
/// The seek command. Instructs users to seek to a specified time.
///
- Seek = 2
+ Seek = 3
}
}