aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/SyncPlay
diff options
context:
space:
mode:
authorIonut Andrei Oanca <oancaionutandrei@gmail.com>2020-09-24 23:04:21 +0200
committerIonut Andrei Oanca <oancaionutandrei@gmail.com>2020-10-16 12:06:29 +0200
commit8819a9d478e6fc11dbfdcff80d9a2dc175953373 (patch)
tree8a159745dd08ebfa6d83e881c8eb6a07df0a589d /Emby.Server.Implementations/SyncPlay
parented2eabec16aafdf795f5ea4f8834ffdc74bc149f (diff)
Add playlist-sync and group-wait to SyncPlay
Diffstat (limited to 'Emby.Server.Implementations/SyncPlay')
-rw-r--r--Emby.Server.Implementations/SyncPlay/GroupController.cs681
-rw-r--r--Emby.Server.Implementations/SyncPlay/GroupStates/AbstractGroupState.cs218
-rw-r--r--Emby.Server.Implementations/SyncPlay/GroupStates/IdleGroupState.cs121
-rw-r--r--Emby.Server.Implementations/SyncPlay/GroupStates/PausedGroupState.cs193
-rw-r--r--Emby.Server.Implementations/SyncPlay/GroupStates/PlayingGroupState.cs133
-rw-r--r--Emby.Server.Implementations/SyncPlay/GroupStates/WaitingGroupState.cs653
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayController.cs282
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs129
8 files changed, 1927 insertions, 483 deletions
diff --git a/Emby.Server.Implementations/SyncPlay/GroupController.cs b/Emby.Server.Implementations/SyncPlay/GroupController.cs
new file mode 100644
index 000000000..ee2e9eb8f
--- /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
+{
+ /// <summary>
+ /// Class SyncPlayGroupController.
+ /// </summary>
+ /// <remarks>
+ /// Class is not thread-safe, external locking is required when accessing methods.
+ /// </remarks>
+ public class SyncPlayGroupController : ISyncPlayGroupController, ISyncPlayStateContext
+ {
+ /// <summary>
+ /// Gets the default ping value used for sessions.
+ /// </summary>
+ public long DefaultPing { get; } = 500;
+
+ /// <summary>
+ /// The logger.
+ /// </summary>
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// The user manager.
+ /// </summary>
+ private readonly IUserManager _userManager;
+
+ /// <summary>
+ /// The session manager.
+ /// </summary>
+ private readonly ISessionManager _sessionManager;
+
+ /// <summary>
+ /// The library manager.
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// The SyncPlay manager.
+ /// </summary>
+ private readonly ISyncPlayManager _syncPlayManager;
+
+ /// <summary>
+ /// Internal group state.
+ /// </summary>
+ /// <value>The group's state.</value>
+ private ISyncPlayState State;
+
+ /// <summary>
+ /// Gets the group identifier.
+ /// </summary>
+ /// <value>The group identifier.</value>
+ public Guid GroupId { get; } = Guid.NewGuid();
+
+ /// <summary>
+ /// Gets the group name.
+ /// </summary>
+ /// <value>The group name.</value>
+ public string GroupName { get; private set; }
+
+ /// <summary>
+ /// Gets the group identifier.
+ /// </summary>
+ /// <value>The group identifier.</value>
+ public PlayQueueManager PlayQueue { get; } = new PlayQueueManager();
+
+ /// <summary>
+ /// Gets or sets the runtime ticks of current playing item.
+ /// </summary>
+ /// <value>The runtime ticks of current playing item.</value>
+ public long RunTimeTicks { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the position ticks.
+ /// </summary>
+ /// <value>The position ticks.</value>
+ public long PositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last activity.
+ /// </summary>
+ /// <value>The last activity.</value>
+ public DateTime LastActivity { get; set; }
+
+ /// <summary>
+ /// Gets the participants.
+ /// </summary>
+ /// <value>The participants, or members of the group.</value>
+ public Dictionary<string, GroupMember> Participants { get; } =
+ new Dictionary<string, GroupMember>(StringComparer.OrdinalIgnoreCase);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SyncPlayGroupController" /> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="userManager">The user manager.</param>
+ /// <param name="sessionManager">The session manager.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="syncPlayManager">The SyncPlay manager.</param>
+ 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);
+ }
+
+ /// <summary>
+ /// Checks if a session is in this group.
+ /// </summary>
+ /// <param name="sessionId">The session id to check.</param>
+ /// <returns><c>true</c> if the session is in this group; <c>false</c> otherwise.</returns>
+ private bool ContainsSession(string sessionId)
+ {
+ return Participants.ContainsKey(sessionId);
+ }
+
+ /// <summary>
+ /// Adds the session to the group.
+ /// </summary>
+ /// <param name="session">The session.</param>
+ private void AddSession(SessionInfo session)
+ {
+ Participants.TryAdd(
+ session.Id,
+ new GroupMember
+ {
+ Session = session,
+ Ping = DefaultPing,
+ IsBuffering = false
+ });
+ }
+
+ /// <summary>
+ /// Removes the session from the group.
+ /// </summary>
+ /// <param name="session">The session.</param>
+ private void RemoveSession(SessionInfo session)
+ {
+ Participants.Remove(session.Id);
+ }
+
+ /// <summary>
+ /// Filters sessions of this group.
+ /// </summary>
+ /// <param name="from">The current session.</param>
+ /// <param name="type">The filtering type.</param>
+ /// <returns>The array of sessions matching the filter.</returns>
+ 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<SessionInfo>();
+ }
+ }
+
+ 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();
+ }
+
+ /// <inheritdoc />
+ public bool IsGroupEmpty() => Participants.Count == 0;
+
+ /// <inheritdoc />
+ 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());
+ }
+
+ /// <inheritdoc />
+ 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());
+ }
+
+ /// <inheritdoc />
+ 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());
+ }
+
+ /// <inheritdoc />
+ 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());
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ 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)
+ };
+ }
+
+ /// <inheritdoc />
+ public bool HasAccessToPlayQueue(User user)
+ {
+ var items = PlayQueue.GetPlaylist().Select(item => item.ItemId).ToArray();
+ return HasAccessToQueue(user, items);
+ }
+
+ /// <inheritdoc />
+ public void SetIgnoreGroupWait(SessionInfo session, bool ignoreGroupWait)
+ {
+ if (!ContainsSession(session.Id))
+ {
+ return;
+ }
+
+ Participants[session.Id].IgnoreGroupWait = ignoreGroupWait;
+ }
+
+ /// <inheritdoc />
+ public void SetState(ISyncPlayState state)
+ {
+ _logger.LogInformation("SetState: {0} switching from {1} to {2}.", GroupId.ToString(), State.GetGroupState(), state.GetGroupState());
+ this.State = state;
+ }
+
+ /// <inheritdoc />
+ public Task SendGroupUpdate<T>(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken)
+ {
+ IEnumerable<Task> GetTasks()
+ {
+ foreach (var session in FilterSessions(from, type))
+ {
+ yield return _sessionManager.SendSyncPlayGroupUpdate(session, message, cancellationToken);
+ }
+ }
+
+ return Task.WhenAll(GetTasks());
+ }
+
+ /// <inheritdoc />
+ public Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken cancellationToken)
+ {
+ IEnumerable<Task> GetTasks()
+ {
+ foreach (var session in FilterSessions(from, type))
+ {
+ yield return _sessionManager.SendSyncPlayCommand(session, message, cancellationToken);
+ }
+ }
+
+ return Task.WhenAll(GetTasks());
+ }
+
+ /// <inheritdoc />
+ 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)
+ };
+ }
+
+ /// <inheritdoc />
+ public GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data)
+ {
+ return new GroupUpdate<T>()
+ {
+ GroupId = GroupId.ToString(),
+ Type = type,
+ Data = data
+ };
+ }
+
+ /// <inheritdoc />
+ public string DateToUTCString(DateTime dateTime)
+ {
+ return dateTime.ToUniversalTime().ToString("o");
+ }
+
+ /// <inheritdoc />
+ public long SanitizePositionTicks(long? positionTicks)
+ {
+ var ticks = positionTicks ?? 0;
+ ticks = ticks >= 0 ? ticks : 0;
+ ticks = ticks > RunTimeTicks ? RunTimeTicks : ticks;
+ return ticks;
+ }
+
+ /// <inheritdoc />
+ public void UpdatePing(SessionInfo session, long ping)
+ {
+ if (Participants.TryGetValue(session.Id, out GroupMember value))
+ {
+ value.Ping = ping;
+ }
+ }
+
+ /// <inheritdoc />
+ public long GetHighestPing()
+ {
+ long max = long.MinValue;
+ foreach (var session in Participants.Values)
+ {
+ max = Math.Max(max, session.Ping);
+ }
+
+ return max;
+ }
+
+ /// <inheritdoc />
+ public void SetBuffering(SessionInfo session, bool isBuffering)
+ {
+ if (Participants.TryGetValue(session.Id, out GroupMember value))
+ {
+ value.IsBuffering = isBuffering;
+ }
+ }
+
+ /// <inheritdoc />
+ public void SetAllBuffering(bool isBuffering)
+ {
+ foreach (var session in Participants.Values)
+ {
+ session.IsBuffering = isBuffering;
+ }
+ }
+
+ /// <inheritdoc />
+ public bool IsBuffering()
+ {
+ foreach (var session in Participants.Values)
+ {
+ if (session.IsBuffering && !session.IgnoreGroupWait)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// <inheritdoc />
+ 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;
+ }
+
+ /// <inheritdoc />
+ 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;
+ }
+
+ /// <inheritdoc />
+ 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;
+ }
+
+ /// <inheritdoc />
+ public bool MoveItemInPlayQueue(string playlistItemId, int newIndex)
+ {
+ return PlayQueue.MovePlaylistItem(playlistItemId, newIndex);
+ }
+
+ /// <inheritdoc />
+ 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;
+ }
+
+ /// <inheritdoc />
+ public void RestartCurrentItem()
+ {
+ PositionTicks = 0;
+ LastActivity = DateTime.UtcNow;
+ }
+
+ /// <inheritdoc />
+ 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;
+ }
+ }
+
+ /// <inheritdoc />
+ 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;
+ }
+ }
+
+ /// <inheritdoc />
+ public void SetRepeatMode(string mode) {
+ PlayQueue.SetRepeatMode(mode);
+ }
+
+ /// <inheritdoc />
+ public void SetShuffleMode(string mode) {
+ PlayQueue.SetShuffleMode(mode);
+ }
+
+ /// <inheritdoc />
+ 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 000000000..1f0cb4287
--- /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
+{
+ /// <summary>
+ /// Class AbstractGroupState.
+ /// </summary>
+ /// <remarks>
+ /// Class is not thread-safe, external locking is required when accessing methods.
+ /// </remarks>
+ public abstract class AbstractGroupState : ISyncPlayState
+ {
+ /// <summary>
+ /// The logger.
+ /// </summary>
+ protected readonly ILogger _logger;
+
+ /// <summary>
+ /// Default constructor.
+ /// </summary>
+ public AbstractGroupState(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// Sends a group state update to all group.
+ /// </summary>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="reason">The reason of the state change.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ 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);
+ }
+
+ /// <inheritdoc />
+ public abstract GroupState GetGroupState();
+
+ /// <inheritdoc />
+ public abstract void SessionJoined(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <inheritdoc />
+ public abstract void SessionLeaving(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, IPlaybackGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+ }
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, UnpauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, StopGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, NextTrackGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ public virtual void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PreviousTrackGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ UnhandledRequest(request);
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ 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 000000000..d6b981c58
--- /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
+{
+ /// <summary>
+ /// Class IdleGroupState.
+ /// </summary>
+ /// <remarks>
+ /// Class is not thread-safe, external locking is required when accessing methods.
+ /// </remarks>
+ public class IdleGroupState : AbstractGroupState
+ {
+ /// <summary>
+ /// Default constructor.
+ /// </summary>
+ public IdleGroupState(ILogger logger) : base(logger)
+ {
+ // Do nothing
+ }
+
+ /// <inheritdoc />
+ public override GroupState GetGroupState()
+ {
+ return GroupState.Idle;
+ }
+
+ /// <inheritdoc />
+ public override void SessionJoined(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, GetGroupState(), session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void SessionLeaving(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Do nothing
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, prevState, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, StopGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, prevState, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, prevState, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, prevState, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ SendStopCommand(context, prevState, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ 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 d3bf24f74..39c0511d9 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
/// <remarks>
/// Class is not thread-safe, external locking is required when accessing methods.
/// </remarks>
- public class PausedGroupState : SyncPlayAbstractState
+ public class PausedGroupState : AbstractGroupState
{
+ /// <summary>
+ /// Default constructor.
+ /// </summary>
+ public PausedGroupState(ILogger logger) : base(logger)
+ {
+ // Do nothing
+ }
+
/// <inheritdoc />
public override GroupState GetGroupState()
{
@@ -24,31 +29,56 @@ namespace MediaBrowser.Controller.SyncPlay
}
/// <inheritdoc />
- 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)
+ {
+ // Wait for session to be ready
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.SessionJoined(context, GetGroupState(), session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override void SessionLeaving(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Do nothing
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, UnpauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
{
// Change state
- var playingState = new PlayingGroupState();
+ var playingState = new PlayingGroupState(_logger);
context.SetState(playingState);
- return playingState.HandleRequest(context, true, request, session, cancellationToken);
+ playingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
}
/// <inheritdoc />
- 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)
{
- if (newState)
+ if (!prevState.Equals(GetGroupState()))
{
- GroupInfo group = context.GetGroup();
-
// 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;
}
/// <inheritdoc />
- 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);
}
/// <inheritdoc />
- 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();
-
- if (newState)
- {
- // Pause group and compute the media playback position
- var currentTime = DateTime.UtcNow;
- var elapsedTime = currentTime - group.LastActivity;
- group.LastActivity = currentTime;
- group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
-
- group.SetBuffering(session, true);
+ // Change state
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
- // Send pause command to all non-buffering sessions
- var command = context.NewSyncPlayCommand(SendCommandType.Pause);
- context.SendCommand(session, SyncPlayBroadcastType.AllReady, command, cancellationToken);
+ /// <inheritdoc />
+ 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);
+ }
- var updateOthers = context.NewSyncPlayGroupUpdate(GroupUpdateType.GroupWait, session.UserName);
- context.SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
- }
- else
+ /// <inheritdoc />
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ if (prevState.Equals(GetGroupState()))
{
- // 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);
+ }
}
/// <inheritdoc />
- 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();
-
- 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);
- }
+ // Change state
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
+ }
- return true;
+ /// <inheritdoc />
+ 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 42c7779c1..e2909ff91 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
/// <remarks>
/// Class is not thread-safe, external locking is required when accessing methods.
/// </remarks>
- public class PlayingGroupState : SyncPlayAbstractState
+ public class PlayingGroupState : AbstractGroupState
{
+ /// <summary>
+ /// Ignore requests for buffering.
+ /// </summary>
+ public bool IgnoreBuffering { get; set; }
+
+ /// <summary>
+ /// Default constructor.
+ /// </summary>
+ public PlayingGroupState(ILogger logger) : base(logger)
+ {
+ // Do nothing
+ }
+
/// <inheritdoc />
public override GroupState GetGroupState()
{
@@ -24,71 +34,132 @@ namespace MediaBrowser.Controller.SyncPlay
}
/// <inheritdoc />
- 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)
+ /// <inheritdoc />
+ public override void SessionLeaving(ISyncPlayStateContext context, GroupState prevState, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Do nothing
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ 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;
}
/// <inheritdoc />
- 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);
}
/// <inheritdoc />
- 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);
}
/// <inheritdoc />
- 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);
+ }
+
+ /// <inheritdoc />
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ if (IgnoreBuffering)
+ {
+ return;
+ }
+
+ // Change state
+ var waitingState = new WaitingGroupState(_logger);
+ context.SetState(waitingState);
+ waitingState.HandleRequest(context, GetGroupState(), request, session, cancellationToken);
}
/// <inheritdoc />
- public override bool HandleRequest(ISyncPlayStateContext context, bool newState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ public override void HandleRequest(ISyncPlayStateContext context, GroupState prevState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
{
- // Group was not waiting, make sure client has latest state
- var command = context.NewSyncPlayCommand(SendCommandType.Play);
- context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+ 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);
+ }
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
- return true;
+ /// <inheritdoc />
+ 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 000000000..9d839b268
--- /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
+{
+ /// <summary>
+ /// Class WaitingGroupState.
+ /// </summary>
+ /// <remarks>
+ /// Class is not thread-safe, external locking is required when accessing methods.
+ /// </remarks>
+ public class WaitingGroupState : AbstractGroupState
+ {
+ /// <summary>
+ /// Tells the state to switch to after buffering is done.
+ /// </summary>
+ public bool ResumePlaying { get; set; } = false;
+
+ /// <summary>
+ /// Whether the initial state has been set.
+ /// </summary>
+ private bool InitialStateSet { get; set; } = false;
+
+ /// <summary>
+ /// The group state before the first ever event.
+ /// </summary>
+ private GroupState InitialState { get; set; }
+
+ /// <summary>
+ /// Default constructor.
+ /// </summary>
+ public WaitingGroupState(ILogger logger) : base(logger)
+ {
+ // Do nothing
+ }
+
+ /// <inheritdoc />
+ public override GroupState GetGroupState()
+ {
+ return GroupState.Waiting;
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ 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());
+ }
+ }
+ }
+
+ /// <inheritdoc />
+ 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());
+ }
+
+ /// <inheritdoc />
+ 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());
+ }
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+ }
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ 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());
+ }
+ }
+ }
+
+ /// <inheritdoc />
+ 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());
+ }
+ }
+
+ /// <inheritdoc />
+ 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 225be7430..000000000
--- 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
-{
- /// <summary>
- /// Class SyncPlayController.
- /// </summary>
- /// <remarks>
- /// Class is not thread-safe, external locking is required when accessing methods.
- /// </remarks>
- public class SyncPlayController : ISyncPlayController, ISyncPlayStateContext
- {
- /// <summary>
- /// The session manager.
- /// </summary>
- private readonly ISessionManager _sessionManager;
-
- /// <summary>
- /// The SyncPlay manager.
- /// </summary>
- private readonly ISyncPlayManager _syncPlayManager;
-
- /// <summary>
- /// The logger.
- /// </summary>
- private readonly ILogger _logger;
-
- /// <summary>
- /// The group to manage.
- /// </summary>
- private readonly GroupInfo _group = new GroupInfo();
-
- /// <summary>
- /// Internal group state.
- /// </summary>
- /// <value>The group's state.</value>
- private ISyncPlayState State = new PausedGroupState();
-
- /// <inheritdoc />
- public GroupInfo GetGroup()
- {
- return _group;
- }
-
- /// <inheritdoc />
- public void SetState(ISyncPlayState state)
- {
- _logger.LogInformation("SetState: {0} -> {1}.", State.GetGroupState(), state.GetGroupState());
- this.State = state;
- }
-
- /// <inheritdoc />
- public Guid GetGroupId() => _group.GroupId;
-
- /// <inheritdoc />
- public Guid GetPlayingItemId() => _group.PlayingItem.Id;
-
- /// <inheritdoc />
- public bool IsGroupEmpty() => _group.IsEmpty();
-
- /// <summary>
- /// Initializes a new instance of the <see cref="SyncPlayController" /> class.
- /// </summary>
- /// <param name="sessionManager">The session manager.</param>
- /// <param name="syncPlayManager">The SyncPlay manager.</param>
- public SyncPlayController(
- ISessionManager sessionManager,
- ISyncPlayManager syncPlayManager,
- ILogger logger)
- {
- _sessionManager = sessionManager;
- _syncPlayManager = syncPlayManager;
- _logger = logger;
- }
-
- /// <summary>
- /// Filters sessions of this group.
- /// </summary>
- /// <param name="from">The current session.</param>
- /// <param name="type">The filtering type.</param>
- /// <value>The array of sessions matching the filter.</value>
- 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<SessionInfo>();
- }
- }
-
- /// <inheritdoc />
- public Task SendGroupUpdate<T>(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken)
- {
- IEnumerable<Task> GetTasks()
- {
- foreach (var session in FilterSessions(from, type))
- {
- yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id, message, cancellationToken);
- }
- }
-
- return Task.WhenAll(GetTasks());
- }
-
- /// <inheritdoc />
- public Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken cancellationToken)
- {
- IEnumerable<Task> GetTasks()
- {
- foreach (var session in FilterSessions(from, type))
- {
- yield return _sessionManager.SendSyncPlayCommand(session.Id, message, cancellationToken);
- }
- }
-
- return Task.WhenAll(GetTasks());
- }
-
- /// <inheritdoc />
- 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)
- };
- }
-
- /// <inheritdoc />
- public GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data)
- {
- return new GroupUpdate<T>()
- {
- GroupId = _group.GroupId.ToString(),
- Type = type,
- Data = data
- };
- }
-
- /// <inheritdoc />
- public string DateToUTCString(DateTime _date)
- {
- return _date.ToUniversalTime().ToString("o");
- }
-
- /// <inheritdoc />
- public long SanitizePositionTicks(long? positionTicks)
- {
- var ticks = positionTicks ?? 0;
- ticks = ticks >= 0 ? ticks : 0;
- if (_group.PlayingItem != null)
- {
- var runTimeTicks = _group.PlayingItem.RunTimeTicks ?? 0;
- ticks = ticks > runTimeTicks ? runTimeTicks : ticks;
- }
-
- return ticks;
- }
-
- /// <inheritdoc />
- public void CreateGroup(SessionInfo session, CancellationToken cancellationToken)
- {
- _group.AddSession(session);
- _syncPlayManager.AddSessionToGroup(session, this);
-
- State = new PausedGroupState();
-
- _group.PlayingItem = session.FullNowPlayingItem;
- // 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);
- }
-
- /// <inheritdoc />
- 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);
- }
- }
-
- /// <inheritdoc />
- 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);
- }
-
- /// <inheritdoc />
- 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
- }
-
- /// <inheritdoc />
- 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 b85f3c149..a8e30a9ec 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
/// <summary>
/// The map between sessions and groups.
/// </summary>
- private readonly Dictionary<string, ISyncPlayController> _sessionToGroupMap =
- new Dictionary<string, ISyncPlayController>(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary<string, ISyncPlayGroupController> _sessionToGroupMap =
+ new Dictionary<string, ISyncPlayGroupController>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// The groups.
/// </summary>
- private readonly Dictionary<Guid, ISyncPlayController> _groups =
- new Dictionary<Guid, ISyncPlayController>();
+ private readonly Dictionary<Guid, ISyncPlayGroupController> _groups =
+ new Dictionary<Guid, ISyncPlayGroupController>();
/// <summary>
/// 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.
/// </summary>
/// <value>All groups.</value>
- public IEnumerable<ISyncPlayController> Groups => _groups.Values;
+ public IEnumerable<ISyncPlayGroupController> Groups => _groups.Values;
/// <inheritdoc />
public void Dispose()
@@ -103,13 +103,15 @@ namespace Emby.Server.Implementations.SyncPlay
return;
}
+ _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
_sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
+ _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
_sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
_disposed = true;
}
- private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
+ private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e)
{
var session = e.SessionInfo;
if (!IsSessionInGroup(session))
@@ -117,52 +119,60 @@ namespace Emby.Server.Implementations.SyncPlay
return;
}
- LeaveGroup(session, CancellationToken.None);
+ var groupId = GetSessionGroup(session) ?? Guid.Empty;
+ var request = new JoinGroupRequest()
+ {
+ GroupId = groupId
+ };
+ JoinGroup(session, groupId, request, CancellationToken.None);
}
- private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e)
+ private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
{
- var session = e.Session;
+ var session = e.SessionInfo;
if (!IsSessionInGroup(session))
{
return;
}
- LeaveGroup(session, CancellationToken.None);
+ // TODO: probably remove this event, not used at the moment
}
- private bool IsSessionInGroup(SessionInfo session)
+ private void OnSessionManagerPlaybackStart(object sender, PlaybackProgressEventArgs e)
{
- return _sessionToGroupMap.ContainsKey(session.Id);
+ var session = e.Session;
+ if (!IsSessionInGroup(session))
+ {
+ return;
+ }
+
+ // TODO: probably remove this event, not used at the moment
}
- private bool HasAccessToItem(User user, Guid itemId)
+ private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e)
{
- var item = _libraryManager.GetItemById(itemId);
-
- // Check ParentalRating access
- var hasParentalRatingAccess = !user.MaxParentalAgeRating.HasValue
- || item.InheritedParentalRatingValue <= user.MaxParentalAgeRating;
-
- if (!user.HasPermission(PermissionKind.EnableAllFolders) && hasParentalRatingAccess)
+ var session = e.Session;
+ if (!IsSessionInGroup(session))
{
- var collections = _libraryManager.GetCollectionFolders(item).Select(
- folder => folder.Id.ToString("N", CultureInfo.InvariantCulture));
-
- return collections.Intersect(user.GetPreference(PreferenceKind.EnabledFolders)).Any();
+ return;
}
- return hasParentalRatingAccess;
+ // TODO: probably remove this event, not used at the moment
+ }
+
+ private bool IsSessionInGroup(SessionInfo session)
+ {
+ return _sessionToGroupMap.ContainsKey(session.Id);
}
private Guid? GetSessionGroup(SessionInfo session)
{
_sessionToGroupMap.TryGetValue(session.Id, out var group);
- return group?.GetGroupId();
+ return group?.GroupId;
}
/// <inheritdoc />
- 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<string>()
{
- 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 _);
}
}
}
/// <inheritdoc />
- public List<GroupInfoDto> ListGroups(SessionInfo session, Guid filterItemId)
+ public List<GroupInfoDto> ListGroups(SessionInfo session)
{
var user = _userManager.GetUserById(session.UserId);
@@ -295,20 +304,9 @@ namespace Emby.Server.Implementations.SyncPlay
return new List<GroupInfoDto>();
}
- // 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();
}
/// <inheritdoc />
@@ -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
}
/// <inheritdoc />
- 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
}
/// <inheritdoc />
- 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!");
}