From b7eb4da04e0cbb820becc9022975f69aed4f8531 Mon Sep 17 00:00:00 2001 From: Ionut Andrei Oanca Date: Thu, 3 Dec 2020 21:01:18 +0100 Subject: Rename GroupController into Group --- Emby.Server.Implementations/SyncPlay/Group.cs | 664 +++++++++++++++++++++ .../SyncPlay/GroupController.cs | 651 -------------------- .../SyncPlay/SyncPlayManager.cs | 29 +- 3 files changed, 679 insertions(+), 665 deletions(-) create mode 100644 Emby.Server.Implementations/SyncPlay/Group.cs delete mode 100644 Emby.Server.Implementations/SyncPlay/GroupController.cs (limited to 'Emby.Server.Implementations') diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs new file mode 100644 index 000000000..e32f5e25d --- /dev/null +++ b/Emby.Server.Implementations/SyncPlay/Group.cs @@ -0,0 +1,664 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Controller.SyncPlay.GroupStates; +using MediaBrowser.Controller.SyncPlay.Queue; +using MediaBrowser.Controller.SyncPlay.Requests; +using MediaBrowser.Model.SyncPlay; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.SyncPlay +{ + /// + /// Class Group. + /// + /// + /// Class is not thread-safe, external locking is required when accessing methods. + /// + public class Group : IGroupStateContext + { + /// + /// The logger. + /// + private readonly ILogger _logger; + + /// + /// The logger factory. + /// + private readonly ILoggerFactory _loggerFactory; + + /// + /// The user manager. + /// + private readonly IUserManager _userManager; + + /// + /// The session manager. + /// + private readonly ISessionManager _sessionManager; + + /// + /// The library manager. + /// + private readonly ILibraryManager _libraryManager; + + /// + /// The participants, or members of the group. + /// + private readonly Dictionary _participants = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// The internal group state. + /// + private IGroupState _state; + + /// + /// Initializes a new instance of the class. + /// + /// The logger factory. + /// The user manager. + /// The session manager. + /// The library manager. + public Group( + ILoggerFactory loggerFactory, + IUserManager userManager, + ISessionManager sessionManager, + ILibraryManager libraryManager) + { + _loggerFactory = loggerFactory; + _userManager = userManager; + _sessionManager = sessionManager; + _libraryManager = libraryManager; + _logger = loggerFactory.CreateLogger(); + + _state = new IdleGroupState(loggerFactory); + } + + /// + /// Gets the default ping value used for sessions. + /// + /// The default ping. + public long DefaultPing { get; } = 500; + + /// + /// Gets the maximum time offset error accepted for dates reported by clients, in milliseconds. + /// + /// The maximum time offset error. + public long TimeSyncOffset { get; } = 2000; + + /// + /// Gets the maximum offset error accepted for position reported by clients, in milliseconds. + /// + /// The maximum offset error. + public long MaxPlaybackOffset { get; } = 500; + + /// + /// Gets the group identifier. + /// + /// The group identifier. + public Guid GroupId { get; } = Guid.NewGuid(); + + /// + /// Gets the group name. + /// + /// The group name. + public string GroupName { get; private set; } + + /// + /// Gets the group identifier. + /// + /// The group identifier. + public PlayQueueManager PlayQueue { get; } = new PlayQueueManager(); + + /// + /// Gets the runtime ticks of current playing item. + /// + /// The runtime ticks of current playing item. + public long RunTimeTicks { get; private set; } + + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + public long PositionTicks { get; set; } + + /// + /// Gets or sets the last activity. + /// + /// The last activity. + public DateTime LastActivity { get; set; } + + /// + /// Adds the session to the group. + /// + /// The session. + private void AddSession(SessionInfo session) + { + _participants.TryAdd( + session.Id, + new GroupMember(session) + { + Ping = DefaultPing, + IsBuffering = false + }); + } + + /// + /// Removes the session from the group. + /// + /// The session. + private void RemoveSession(SessionInfo session) + { + _participants.Remove(session.Id); + } + + /// + /// Filters sessions of this group. + /// + /// The current session. + /// The filtering type. + /// The list of sessions matching the filter. + private IEnumerable FilterSessions(SessionInfo from, SyncPlayBroadcastType type) + { + return type switch + { + SyncPlayBroadcastType.CurrentSession => new SessionInfo[] { from }, + SyncPlayBroadcastType.AllGroup => _participants + .Values + .Select(session => session.Session), + SyncPlayBroadcastType.AllExceptCurrentSession => _participants + .Values + .Select(session => session.Session) + .Where(session => !session.Id.Equals(from.Id, StringComparison.OrdinalIgnoreCase)), + SyncPlayBroadcastType.AllReady => _participants + .Values + .Where(session => !session.IsBuffering) + .Select(session => session.Session), + _ => Enumerable.Empty() + }; + } + + /// + /// Checks if a given user can access all items of a given queue, that is, + /// the user has the required minimum parental access and has access to all required folders. + /// + /// The user. + /// The queue. + /// true if the user can access all the items in the queue, false otherwise. + private bool HasAccessToQueue(User user, IReadOnlyList queue) + { + // Check if queue is empty. + if (queue == null || queue.Count == 0) + { + return true; + } + + foreach (var itemId in queue) + { + var item = _libraryManager.GetItemById(itemId); + if (!item.IsVisibleStandalone(user)) + { + return false; + } + } + + return true; + } + + private bool AllUsersHaveAccessToQueue(IReadOnlyList queue) + { + // Check if queue is empty. + if (queue == null || queue.Count == 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(); + } + + /// + /// Checks if the group is empty. + /// + /// true if the group is empty, false otherwise. + public bool IsGroupEmpty() => _participants.Count == 0; + + /// + /// Initializes the group with the session's info. + /// + /// The session. + /// The request. + /// The cancellation token. + public void CreateGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken) + { + GroupName = request.GroupName; + AddSession(session); + + var sessionIsPlayingAnItem = session.FullNowPlayingItem != null; + + RestartCurrentItem(); + + if (sessionIsPlayingAnItem) + { + var playlist = session.NowPlayingQueue.Select(item => item.Id).ToList(); + PlayQueue.Reset(); + PlayQueue.SetPlaylist(playlist); + PlayQueue.SetPlayingItemById(session.FullNowPlayingItem.Id); + RunTimeTicks = session.FullNowPlayingItem.RunTimeTicks ?? 0; + PositionTicks = session.PlayState.PositionTicks ?? 0; + + // Maintain playstate. + var waitingState = new WaitingGroupState(_loggerFactory) + { + ResumePlaying = !session.PlayState.IsPaused + }; + SetState(waitingState); + } + + var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, GetInfo()); + SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); + + _state.SessionJoined(this, _state.Type, session, cancellationToken); + + _logger.LogInformation("Session {SessionId} created group {GroupId}.", session.Id, GroupId.ToString()); + } + + /// + /// Adds the session to the group. + /// + /// The session. + /// The request. + /// The cancellation token. + public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken) + { + AddSession(session); + + 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.Type, session, cancellationToken); + + _logger.LogInformation("Session {SessionId} joined group {GroupId}.", session.Id, GroupId.ToString()); + } + + /// + /// Removes the session from the group. + /// + /// The session. + /// The request. + /// The cancellation token. + public void SessionLeave(SessionInfo session, LeaveGroupRequest request, CancellationToken cancellationToken) + { + _state.SessionLeaving(this, _state.Type, session, cancellationToken); + + RemoveSession(session); + + 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("Session {SessionId} left group {GroupId}.", session.Id, GroupId.ToString()); + } + + /// + /// Handles the requested action by the session. + /// + /// The session. + /// The requested action. + /// The cancellation token. + public void HandleRequest(SessionInfo session, IGroupPlaybackRequest 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("Session {SessionId} requested {RequestType} in group {GroupId} that is {StateType}.", session.Id, request.Action, GroupId.ToString(), _state.Type); + request.Apply(this, _state, session, cancellationToken); + } + + /// + /// Gets the info about the group for the clients. + /// + /// The group info for the clients. + public GroupInfoDto GetInfo() + { + var participants = _participants.Values.Select(session => session.Session.UserName).Distinct().ToList(); + return new GroupInfoDto(GroupId, GroupName, _state.Type, participants, DateTime.UtcNow); + } + + /// + /// Checks if a user has access to all content in the play queue. + /// + /// The user. + /// true if the user can access the play queue; false otherwise. + public bool HasAccessToPlayQueue(User user) + { + var items = PlayQueue.GetPlaylist().Select(item => item.ItemId).ToList(); + return HasAccessToQueue(user, items); + } + + /// + public void SetIgnoreGroupWait(SessionInfo session, bool ignoreGroupWait) + { + if (_participants.TryGetValue(session.Id, out GroupMember value)) + { + value.IgnoreGroupWait = ignoreGroupWait; + } + } + + /// + public void SetState(IGroupState state) + { + _logger.LogInformation("Group {GroupId} switching from {FromStateType} to {ToStateType}.", GroupId.ToString(), _state.Type, state.Type); + this._state = state; + } + + /// + public Task SendGroupUpdate(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate message, CancellationToken cancellationToken) + { + IEnumerable GetTasks() + { + foreach (var session in FilterSessions(from, type)) + { + yield return _sessionManager.SendSyncPlayGroupUpdate(session, message, cancellationToken); + } + } + + return Task.WhenAll(GetTasks()); + } + + /// + public Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken cancellationToken) + { + IEnumerable GetTasks() + { + foreach (var session in FilterSessions(from, type)) + { + yield return _sessionManager.SendSyncPlayCommand(session, message, cancellationToken); + } + } + + return Task.WhenAll(GetTasks()); + } + + /// + public SendCommand NewSyncPlayCommand(SendCommandType type) + { + return new SendCommand( + GroupId, + PlayQueue.GetPlayingItemPlaylistId(), + LastActivity, + type, + PositionTicks, + DateTime.UtcNow); + } + + /// + public GroupUpdate NewSyncPlayGroupUpdate(GroupUpdateType type, T data) + { + return new GroupUpdate(GroupId, type, data); + } + + /// + public long SanitizePositionTicks(long? positionTicks) + { + var ticks = positionTicks ?? 0; + return Math.Clamp(ticks, 0, RunTimeTicks); + } + + /// + public void UpdatePing(SessionInfo session, long ping) + { + if (_participants.TryGetValue(session.Id, out GroupMember value)) + { + value.Ping = ping; + } + } + + /// + public long GetHighestPing() + { + long max = long.MinValue; + foreach (var session in _participants.Values) + { + max = Math.Max(max, session.Ping); + } + + return max; + } + + /// + public void SetBuffering(SessionInfo session, bool isBuffering) + { + if (_participants.TryGetValue(session.Id, out GroupMember value)) + { + value.IsBuffering = isBuffering; + } + } + + /// + public void SetAllBuffering(bool isBuffering) + { + foreach (var session in _participants.Values) + { + session.IsBuffering = isBuffering; + } + } + + /// + public bool IsBuffering() + { + foreach (var session in _participants.Values) + { + if (session.IsBuffering && !session.IgnoreGroupWait) + { + return true; + } + } + + return false; + } + + /// + public bool SetPlayQueue(IReadOnlyList playQueue, int playingItemPosition, long startPositionTicks) + { + // Ignore on empty queue or invalid item position. + if (playQueue.Count == 0 || playingItemPosition >= playQueue.Count || playingItemPosition < 0) + { + return false; + } + + // Check if participants can access the new playing queue. + if (!AllUsersHaveAccessToQueue(playQueue)) + { + return false; + } + + PlayQueue.Reset(); + PlayQueue.SetPlaylist(playQueue); + PlayQueue.SetPlayingItemByIndex(playingItemPosition); + var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId()); + RunTimeTicks = item.RunTimeTicks ?? 0; + PositionTicks = startPositionTicks; + LastActivity = DateTime.UtcNow; + + return true; + } + + /// + public bool SetPlayingItem(string playlistItemId) + { + var itemFound = PlayQueue.SetPlayingItemByPlaylistId(playlistItemId); + + if (itemFound) + { + var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId()); + RunTimeTicks = item.RunTimeTicks ?? 0; + } + else + { + RunTimeTicks = 0; + } + + RestartCurrentItem(); + + return itemFound; + } + + /// + public bool RemoveFromPlayQueue(IReadOnlyList playlistItemIds) + { + var playingItemRemoved = PlayQueue.RemoveFromPlaylist(playlistItemIds); + if (playingItemRemoved) + { + var itemId = PlayQueue.GetPlayingItemId(); + if (!itemId.Equals(Guid.Empty)) + { + var item = _libraryManager.GetItemById(itemId); + RunTimeTicks = item.RunTimeTicks ?? 0; + } + else + { + RunTimeTicks = 0; + } + + RestartCurrentItem(); + } + + return playingItemRemoved; + } + + /// + public bool MoveItemInPlayQueue(string playlistItemId, int newIndex) + { + return PlayQueue.MovePlaylistItem(playlistItemId, newIndex); + } + + /// + public bool AddToPlayQueue(IReadOnlyList newItems, GroupQueueMode mode) + { + // Ignore on empty list. + if (newItems.Count == 0) + { + return false; + } + + // Check if participants can access the new playing queue. + if (!AllUsersHaveAccessToQueue(newItems)) + { + return false; + } + + if (mode.Equals(GroupQueueMode.QueueNext)) + { + PlayQueue.QueueNext(newItems); + } + else + { + PlayQueue.Queue(newItems); + } + + return true; + } + + /// + public void RestartCurrentItem() + { + PositionTicks = 0; + LastActivity = DateTime.UtcNow; + } + + /// + public bool NextItemInQueue() + { + var update = PlayQueue.Next(); + if (update) + { + var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId()); + RunTimeTicks = item.RunTimeTicks ?? 0; + RestartCurrentItem(); + return true; + } + else + { + return false; + } + } + + /// + public bool PreviousItemInQueue() + { + var update = PlayQueue.Previous(); + if (update) + { + var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId()); + RunTimeTicks = item.RunTimeTicks ?? 0; + RestartCurrentItem(); + return true; + } + else + { + return false; + } + } + + /// + public void SetRepeatMode(GroupRepeatMode mode) + { + PlayQueue.SetRepeatMode(mode); + } + + /// + public void SetShuffleMode(GroupShuffleMode mode) + { + PlayQueue.SetShuffleMode(mode); + } + + /// + public PlayQueueUpdate GetPlayQueueUpdate(PlayQueueUpdateReason reason) + { + var startPositionTicks = PositionTicks; + + if (_state.Type.Equals(GroupStateType.Playing)) + { + var currentTime = DateTime.UtcNow; + var elapsedTime = currentTime - LastActivity; + // Elapsed time is negative if event happens + // during the delay added to account for latency. + // In this phase clients haven't started the playback yet. + // In other words, LastActivity is in the future, + // when playback unpause is supposed to happen. + // Adjust ticks only if playback actually started. + startPositionTicks += Math.Max(elapsedTime.Ticks, 0); + } + + return new PlayQueueUpdate( + reason, + PlayQueue.LastChange, + PlayQueue.GetPlaylist(), + PlayQueue.PlayingItemIndex, + startPositionTicks, + PlayQueue.ShuffleMode, + PlayQueue.RepeatMode); + } + } +} diff --git a/Emby.Server.Implementations/SyncPlay/GroupController.cs b/Emby.Server.Implementations/SyncPlay/GroupController.cs deleted file mode 100644 index 16acae99e..000000000 --- a/Emby.Server.Implementations/SyncPlay/GroupController.cs +++ /dev/null @@ -1,651 +0,0 @@ -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.Controller.SyncPlay.GroupStates; -using MediaBrowser.Controller.SyncPlay.Queue; -using MediaBrowser.Controller.SyncPlay.Requests; -using MediaBrowser.Model.SyncPlay; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.SyncPlay -{ - /// - /// Class GroupController. - /// - /// - /// Class is not thread-safe, external locking is required when accessing methods. - /// - public class GroupController : IGroupController, IGroupStateContext - { - /// - /// The logger. - /// - private readonly ILogger _logger; - - /// - /// The logger factory. - /// - private readonly ILoggerFactory _loggerFactory; - - /// - /// The user manager. - /// - private readonly IUserManager _userManager; - - /// - /// The session manager. - /// - private readonly ISessionManager _sessionManager; - - /// - /// The library manager. - /// - private readonly ILibraryManager _libraryManager; - - /// - /// The participants, or members of the group. - /// - private readonly Dictionary _participants = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - /// - /// The internal group state. - /// - private IGroupState _state; - - /// - /// Initializes a new instance of the class. - /// - /// The logger factory. - /// The user manager. - /// The session manager. - /// The library manager. - public GroupController( - ILoggerFactory loggerFactory, - IUserManager userManager, - ISessionManager sessionManager, - ILibraryManager libraryManager) - { - _loggerFactory = loggerFactory; - _userManager = userManager; - _sessionManager = sessionManager; - _libraryManager = libraryManager; - _logger = loggerFactory.CreateLogger(); - - _state = new IdleGroupState(loggerFactory); - } - - /// - /// Gets the default ping value used for sessions. - /// - /// The default ping. - public long DefaultPing { get; } = 500; - - /// - /// Gets the maximum time offset error accepted for dates reported by clients, in milliseconds. - /// - /// The maximum time offset error. - public long TimeSyncOffset { get; } = 2000; - - /// - /// Gets the maximum offset error accepted for position reported by clients, in milliseconds. - /// - /// The maximum offset error. - public long MaxPlaybackOffset { get; } = 500; - - /// - /// Gets the group identifier. - /// - /// The group identifier. - public Guid GroupId { get; } = Guid.NewGuid(); - - /// - /// Gets the group name. - /// - /// The group name. - public string GroupName { get; private set; } - - /// - /// Gets the group identifier. - /// - /// The group identifier. - public PlayQueueManager PlayQueue { get; } = new PlayQueueManager(); - - /// - /// Gets the runtime ticks of current playing item. - /// - /// The runtime ticks of current playing item. - public long RunTimeTicks { get; private set; } - - /// - /// Gets or sets the position ticks. - /// - /// The position ticks. - public long PositionTicks { get; set; } - - /// - /// Gets or sets the last activity. - /// - /// The last activity. - public DateTime LastActivity { get; set; } - - /// - /// Adds the session to the group. - /// - /// The session. - private void AddSession(SessionInfo session) - { - _participants.TryAdd( - session.Id, - new GroupMember(session) - { - Ping = DefaultPing, - IsBuffering = false - }); - } - - /// - /// Removes the session from the group. - /// - /// The session. - private void RemoveSession(SessionInfo session) - { - _participants.Remove(session.Id); - } - - /// - /// Filters sessions of this group. - /// - /// The current session. - /// The filtering type. - /// The list of sessions matching the filter. - private IEnumerable FilterSessions(SessionInfo from, SyncPlayBroadcastType type) - { - return type switch - { - SyncPlayBroadcastType.CurrentSession => new SessionInfo[] { from }, - SyncPlayBroadcastType.AllGroup => _participants - .Values - .Select(session => session.Session), - SyncPlayBroadcastType.AllExceptCurrentSession => _participants - .Values - .Select(session => session.Session) - .Where(session => !session.Id.Equals(from.Id, StringComparison.OrdinalIgnoreCase)), - SyncPlayBroadcastType.AllReady => _participants - .Values - .Where(session => !session.IsBuffering) - .Select(session => session.Session), - _ => Enumerable.Empty() - }; - } - - /// - /// Checks if a given user can access all items of a given queue, that is, - /// the user has the required minimum parental access and has access to all required folders. - /// - /// The user. - /// The queue. - /// true if the user can access all the items in the queue, false otherwise. - private bool HasAccessToQueue(User user, IReadOnlyList queue) - { - // Check if queue is empty. - if (queue == null || queue.Count == 0) - { - return true; - } - - foreach (var itemId in queue) - { - var item = _libraryManager.GetItemById(itemId); - if (!item.IsVisibleStandalone(user)) - { - return false; - } - } - - return true; - } - - private bool AllUsersHaveAccessToQueue(IReadOnlyList queue) - { - // Check if queue is empty. - if (queue == null || queue.Count == 0) - { - return true; - } - - // Get list of users. - var users = _participants - .Values - .Select(participant => _userManager.GetUserById(participant.Session.UserId)); - - // Find problematic users. - var usersWithNoAccess = users.Where(user => !HasAccessToQueue(user, queue)); - - // All users must be able to access the queue. - return !usersWithNoAccess.Any(); - } - - /// - public bool IsGroupEmpty() => _participants.Count == 0; - - /// - public void CreateGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken) - { - GroupName = request.GroupName; - AddSession(session); - - var sessionIsPlayingAnItem = session.FullNowPlayingItem != null; - - RestartCurrentItem(); - - if (sessionIsPlayingAnItem) - { - var playlist = session.NowPlayingQueue.Select(item => item.Id).ToList(); - PlayQueue.Reset(); - PlayQueue.SetPlaylist(playlist); - PlayQueue.SetPlayingItemById(session.FullNowPlayingItem.Id); - RunTimeTicks = session.FullNowPlayingItem.RunTimeTicks ?? 0; - PositionTicks = session.PlayState.PositionTicks ?? 0; - - // Maintain playstate. - var waitingState = new WaitingGroupState(_loggerFactory) - { - ResumePlaying = !session.PlayState.IsPaused - }; - SetState(waitingState); - } - - var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, GetInfo()); - SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); - - _state.SessionJoined(this, _state.Type, session, cancellationToken); - - _logger.LogInformation("Session {SessionId} created group {GroupId}.", session.Id, GroupId.ToString()); - } - - /// - public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken) - { - AddSession(session); - - 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.Type, session, cancellationToken); - - _logger.LogInformation("Session {SessionId} joined group {GroupId}.", session.Id, GroupId.ToString()); - } - - /// - public void SessionRestore(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken) - { - var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, GetInfo()); - SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); - - var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName); - SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); - - _state.SessionJoined(this, _state.Type, session, cancellationToken); - - _logger.LogInformation("Session {SessionId} re-joined group {GroupId}.", session.Id, GroupId.ToString()); - } - - /// - public void SessionLeave(SessionInfo session, LeaveGroupRequest request, CancellationToken cancellationToken) - { - _state.SessionLeaving(this, _state.Type, session, cancellationToken); - - RemoveSession(session); - - 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("Session {SessionId} left group {GroupId}.", session.Id, GroupId.ToString()); - } - - /// - public void HandleRequest(SessionInfo session, IGroupPlaybackRequest 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("Session {SessionId} requested {RequestType} in group {GroupId} that is {StateType}.", session.Id, request.Action, GroupId.ToString(), _state.Type); - request.Apply(this, _state, session, cancellationToken); - } - - /// - public GroupInfoDto GetInfo() - { - var participants = _participants.Values.Select(session => session.Session.UserName).Distinct().ToList(); - return new GroupInfoDto(GroupId, GroupName, _state.Type, participants, DateTime.UtcNow); - } - - /// - public bool HasAccessToPlayQueue(User user) - { - var items = PlayQueue.GetPlaylist().Select(item => item.ItemId).ToList(); - return HasAccessToQueue(user, items); - } - - /// - public void SetIgnoreGroupWait(SessionInfo session, bool ignoreGroupWait) - { - if (_participants.TryGetValue(session.Id, out GroupMember value)) - { - value.IgnoreGroupWait = ignoreGroupWait; - } - } - - /// - public void SetState(IGroupState state) - { - _logger.LogInformation("Group {GroupId} switching from {FromStateType} to {ToStateType}.", GroupId.ToString(), _state.Type, state.Type); - this._state = state; - } - - /// - public Task SendGroupUpdate(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate message, CancellationToken cancellationToken) - { - IEnumerable GetTasks() - { - foreach (var session in FilterSessions(from, type)) - { - yield return _sessionManager.SendSyncPlayGroupUpdate(session, message, cancellationToken); - } - } - - return Task.WhenAll(GetTasks()); - } - - /// - public Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken cancellationToken) - { - IEnumerable GetTasks() - { - foreach (var session in FilterSessions(from, type)) - { - yield return _sessionManager.SendSyncPlayCommand(session, message, cancellationToken); - } - } - - return Task.WhenAll(GetTasks()); - } - - /// - public SendCommand NewSyncPlayCommand(SendCommandType type) - { - return new SendCommand( - GroupId, - PlayQueue.GetPlayingItemPlaylistId(), - LastActivity, - type, - PositionTicks, - DateTime.UtcNow); - } - - /// - public GroupUpdate NewSyncPlayGroupUpdate(GroupUpdateType type, T data) - { - return new GroupUpdate(GroupId, type, data); - } - - /// - public long SanitizePositionTicks(long? positionTicks) - { - var ticks = positionTicks ?? 0; - return Math.Clamp(ticks, 0, RunTimeTicks); - } - - /// - public void UpdatePing(SessionInfo session, long ping) - { - if (_participants.TryGetValue(session.Id, out GroupMember value)) - { - value.Ping = ping; - } - } - - /// - public long GetHighestPing() - { - long max = long.MinValue; - foreach (var session in _participants.Values) - { - max = Math.Max(max, session.Ping); - } - - return max; - } - - /// - public void SetBuffering(SessionInfo session, bool isBuffering) - { - if (_participants.TryGetValue(session.Id, out GroupMember value)) - { - value.IsBuffering = isBuffering; - } - } - - /// - public void SetAllBuffering(bool isBuffering) - { - foreach (var session in _participants.Values) - { - session.IsBuffering = isBuffering; - } - } - - /// - public bool IsBuffering() - { - foreach (var session in _participants.Values) - { - if (session.IsBuffering && !session.IgnoreGroupWait) - { - return true; - } - } - - return false; - } - - /// - public bool SetPlayQueue(IReadOnlyList playQueue, int playingItemPosition, long startPositionTicks) - { - // Ignore on empty queue or invalid item position. - if (playQueue.Count == 0 || playingItemPosition >= playQueue.Count || playingItemPosition < 0) - { - return false; - } - - // Check if participants can access the new playing queue. - if (!AllUsersHaveAccessToQueue(playQueue)) - { - return false; - } - - PlayQueue.Reset(); - PlayQueue.SetPlaylist(playQueue); - PlayQueue.SetPlayingItemByIndex(playingItemPosition); - var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId()); - RunTimeTicks = item.RunTimeTicks ?? 0; - PositionTicks = startPositionTicks; - LastActivity = DateTime.UtcNow; - - return true; - } - - /// - public bool SetPlayingItem(string playlistItemId) - { - var itemFound = PlayQueue.SetPlayingItemByPlaylistId(playlistItemId); - - if (itemFound) - { - var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId()); - RunTimeTicks = item.RunTimeTicks ?? 0; - } - else - { - RunTimeTicks = 0; - } - - RestartCurrentItem(); - - return itemFound; - } - - /// - public bool RemoveFromPlayQueue(IReadOnlyList playlistItemIds) - { - var playingItemRemoved = PlayQueue.RemoveFromPlaylist(playlistItemIds); - if (playingItemRemoved) - { - var itemId = PlayQueue.GetPlayingItemId(); - if (!itemId.Equals(Guid.Empty)) - { - var item = _libraryManager.GetItemById(itemId); - RunTimeTicks = item.RunTimeTicks ?? 0; - } - else - { - RunTimeTicks = 0; - } - - RestartCurrentItem(); - } - - return playingItemRemoved; - } - - /// - public bool MoveItemInPlayQueue(string playlistItemId, int newIndex) - { - return PlayQueue.MovePlaylistItem(playlistItemId, newIndex); - } - - /// - public bool AddToPlayQueue(IReadOnlyList newItems, GroupQueueMode mode) - { - // Ignore on empty list. - if (newItems.Count == 0) - { - return false; - } - - // Check if participants can access the new playing queue. - if (!AllUsersHaveAccessToQueue(newItems)) - { - return false; - } - - if (mode.Equals(GroupQueueMode.QueueNext)) - { - PlayQueue.QueueNext(newItems); - } - else - { - PlayQueue.Queue(newItems); - } - - return true; - } - - /// - public void RestartCurrentItem() - { - PositionTicks = 0; - LastActivity = DateTime.UtcNow; - } - - /// - public bool NextItemInQueue() - { - var update = PlayQueue.Next(); - if (update) - { - var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId()); - RunTimeTicks = item.RunTimeTicks ?? 0; - RestartCurrentItem(); - return true; - } - else - { - return false; - } - } - - /// - public bool PreviousItemInQueue() - { - var update = PlayQueue.Previous(); - if (update) - { - var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId()); - RunTimeTicks = item.RunTimeTicks ?? 0; - RestartCurrentItem(); - return true; - } - else - { - return false; - } - } - - /// - public void SetRepeatMode(GroupRepeatMode mode) - { - PlayQueue.SetRepeatMode(mode); - } - - /// - public void SetShuffleMode(GroupShuffleMode mode) - { - PlayQueue.SetShuffleMode(mode); - } - - /// - public PlayQueueUpdate GetPlayQueueUpdate(PlayQueueUpdateReason reason) - { - var startPositionTicks = PositionTicks; - - if (_state.Type.Equals(GroupStateType.Playing)) - { - var currentTime = DateTime.UtcNow; - var elapsedTime = currentTime - LastActivity; - // Elapsed time is negative if event happens - // during the delay added to account for latency. - // In this phase clients haven't started the playback yet. - // In other words, LastActivity is in the future, - // when playback unpause is supposed to happen. - // Adjust ticks only if playback actually started. - startPositionTicks += Math.Max(elapsedTime.Ticks, 0); - } - - return new PlayQueueUpdate( - reason, - PlayQueue.LastChange, - PlayQueue.GetPlaylist(), - PlayQueue.PlayingItemIndex, - startPositionTicks, - PlayQueue.ShuffleMode, - PlayQueue.RepeatMode); - } - } -} diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs index 0410048c4..b2422f8e6 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -44,20 +44,20 @@ namespace Emby.Server.Implementations.SyncPlay /// /// The map between sessions and groups. /// - private readonly Dictionary _sessionToGroupMap = - new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _sessionToGroupMap = + new Dictionary(StringComparer.OrdinalIgnoreCase); /// /// The groups. /// - private readonly Dictionary _groups = - new Dictionary(); + private readonly Dictionary _groups = + new Dictionary(); /// /// Lock used for accessing any group. /// /// - /// Always lock before and before locking on any . + /// Always lock before and before locking on any . /// private readonly object _groupsLock = new object(); @@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.SyncPlay /// Lock used for accessing the session-to-group map. /// /// - /// Always lock after and before locking on any . + /// Always lock after and before locking on any . /// private readonly object _mapsLock = new object(); @@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.SyncPlay LeaveGroup(session, leaveGroupRequest, cancellationToken); } - var group = new GroupController(_loggerFactory, _userManager, _sessionManager, _libraryManager); + var group = new Group(_loggerFactory, _userManager, _sessionManager, _libraryManager); _groups[group.GroupId] = group; AddSessionToGroup(session, group); @@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.SyncPlay // Locking required to access list of groups. lock (_groupsLock) { - _groups.TryGetValue(request.GroupId, out IGroupController group); + _groups.TryGetValue(request.GroupId, out Group group); if (group == null) { @@ -162,7 +162,8 @@ namespace Emby.Server.Implementations.SyncPlay { if (FindJoinedGroupId(session).Equals(request.GroupId)) { - group.SessionRestore(session, request, cancellationToken); + // Restore session. + group.SessionJoin(session, request, cancellationToken); return; } @@ -240,7 +241,7 @@ namespace Emby.Server.Implementations.SyncPlay /// public void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToken) { - IGroupController group; + Group group; lock (_mapsLock) { group = FindJoinedGroup(session); @@ -255,7 +256,7 @@ namespace Emby.Server.Implementations.SyncPlay return; } - // Group lock required as GroupController is not thread-safe. + // Group lock required as Group is not thread-safe. lock (group) { group.HandleRequest(session, request, cancellationToken); @@ -317,7 +318,7 @@ namespace Emby.Server.Implementations.SyncPlay /// /// The session. /// The group. - private IGroupController FindJoinedGroup(SessionInfo session) + private Group FindJoinedGroup(SessionInfo session) { _sessionToGroupMap.TryGetValue(session.Id, out var group); return group; @@ -345,7 +346,7 @@ namespace Emby.Server.Implementations.SyncPlay /// The session. /// The group. /// Thrown when the user is in another group already. - private void AddSessionToGroup(SessionInfo session, IGroupController group) + private void AddSessionToGroup(SessionInfo session, Group group) { if (session == null) { @@ -369,7 +370,7 @@ namespace Emby.Server.Implementations.SyncPlay /// The session. /// The group. /// Thrown when the user is not found in the specified group. - private void RemoveSessionFromGroup(SessionInfo session, IGroupController group) + private void RemoveSessionFromGroup(SessionInfo session, Group group) { if (session == null) { -- cgit v1.2.3