diff options
Diffstat (limited to 'Emby.Server.Implementations/SyncPlay/SyncPlayController.cs')
| -rw-r--r-- | Emby.Server.Implementations/SyncPlay/SyncPlayController.cs | 416 |
1 files changed, 92 insertions, 324 deletions
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs index c98fd6d4a..225be7430 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs @@ -8,6 +8,7 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Controller.SyncPlay; using MediaBrowser.Model.Session; using MediaBrowser.Model.SyncPlay; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.SyncPlay { @@ -17,35 +18,9 @@ namespace Emby.Server.Implementations.SyncPlay /// <remarks> /// Class is not thread-safe, external locking is required when accessing methods. /// </remarks> - public class SyncPlayController : ISyncPlayController + public class SyncPlayController : ISyncPlayController, ISyncPlayStateContext { /// <summary> - /// Used to filter the sessions of a group. - /// </summary> - private enum BroadcastType - { - /// <summary> - /// All sessions will receive the message. - /// </summary> - AllGroup = 0, - - /// <summary> - /// Only the specified session will receive the message. - /// </summary> - CurrentSession = 1, - - /// <summary> - /// All sessions, except the current one, will receive the message. - /// </summary> - AllExceptCurrentSession = 2, - - /// <summary> - /// Only sessions that are not buffering will receive the message. - /// </summary> - AllReady = 3 - } - - /// <summary> /// The session manager. /// </summary> private readonly ISessionManager _sessionManager; @@ -56,21 +31,32 @@ namespace Emby.Server.Implementations.SyncPlay private readonly ISyncPlayManager _syncPlayManager; /// <summary> + /// The logger. + /// </summary> + private readonly ILogger _logger; + + /// <summary> /// The group to manage. /// </summary> private readonly GroupInfo _group = new GroupInfo(); /// <summary> - /// Initializes a new instance of the <see cref="SyncPlayController" /> class. + /// Internal group state. /// </summary> - /// <param name="sessionManager">The session manager.</param> - /// <param name="syncPlayManager">The SyncPlay manager.</param> - public SyncPlayController( - ISessionManager sessionManager, - ISyncPlayManager syncPlayManager) + /// <value>The group's state.</value> + private ISyncPlayState State = new PausedGroupState(); + + /// <inheritdoc /> + public GroupInfo GetGroup() { - _sessionManager = sessionManager; - _syncPlayManager = syncPlayManager; + return _group; + } + + /// <inheritdoc /> + public void SetState(ISyncPlayState state) + { + _logger.LogInformation("SetState: {0} -> {1}.", State.GetGroupState(), state.GetGroupState()); + this.State = state; } /// <inheritdoc /> @@ -83,13 +69,18 @@ namespace Emby.Server.Implementations.SyncPlay public bool IsGroupEmpty() => _group.IsEmpty(); /// <summary> - /// Converts DateTime to UTC string. + /// Initializes a new instance of the <see cref="SyncPlayController" /> class. /// </summary> - /// <param name="date">The date to convert.</param> - /// <value>The UTC string.</value> - private string DateToUTCString(DateTime date) + /// <param name="sessionManager">The session manager.</param> + /// <param name="syncPlayManager">The SyncPlay manager.</param> + public SyncPlayController( + ISessionManager sessionManager, + ISyncPlayManager syncPlayManager, + ILogger logger) { - return date.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture); + _sessionManager = sessionManager; + _syncPlayManager = syncPlayManager; + _logger = logger; } /// <summary> @@ -98,37 +89,30 @@ namespace Emby.Server.Implementations.SyncPlay /// <param name="from">The current session.</param> /// <param name="type">The filtering type.</param> /// <value>The array of sessions matching the filter.</value> - private IEnumerable<SessionInfo> FilterSessions(SessionInfo from, BroadcastType type) + private SessionInfo[] FilterSessions(SessionInfo from, SyncPlayBroadcastType type) { switch (type) { - case BroadcastType.CurrentSession: + case SyncPlayBroadcastType.CurrentSession: return new SessionInfo[] { from }; - case BroadcastType.AllGroup: - return _group.Participants.Values - .Select(session => session.Session); - case BroadcastType.AllExceptCurrentSession: - return _group.Participants.Values - .Select(session => session.Session) - .Where(session => !session.Id.Equals(from.Id, StringComparison.Ordinal)); - case BroadcastType.AllReady: - return _group.Participants.Values - .Where(session => !session.IsBuffering) - .Select(session => session.Session); + case SyncPlayBroadcastType.AllGroup: + return _group.Participants.Values.Select( + session => session.Session).ToArray(); + case SyncPlayBroadcastType.AllExceptCurrentSession: + return _group.Participants.Values.Select( + session => session.Session).Where( + session => !session.Id.Equals(from.Id)).ToArray(); + case SyncPlayBroadcastType.AllReady: + return _group.Participants.Values.Where( + session => !session.IsBuffering).Select( + session => session.Session).ToArray(); default: return Array.Empty<SessionInfo>(); } } - /// <summary> - /// Sends a GroupUpdate message to the interested sessions. - /// </summary> - /// <param name="from">The current session.</param> - /// <param name="type">The filtering type.</param> - /// <param name="message">The message to send.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <value>The task.</value> - private Task SendGroupUpdate<T>(SessionInfo from, BroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken) + /// <inheritdoc /> + public Task SendGroupUpdate<T>(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken) { IEnumerable<Task> GetTasks() { @@ -141,15 +125,8 @@ namespace Emby.Server.Implementations.SyncPlay return Task.WhenAll(GetTasks()); } - /// <summary> - /// Sends a playback command to the interested sessions. - /// </summary> - /// <param name="from">The current session.</param> - /// <param name="type">The filtering type.</param> - /// <param name="message">The message to send.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <value>The task.</value> - private Task SendCommand(SessionInfo from, BroadcastType type, SendCommand message, CancellationToken cancellationToken) + /// <inheritdoc /> + public Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken cancellationToken) { IEnumerable<Task> GetTasks() { @@ -162,12 +139,8 @@ namespace Emby.Server.Implementations.SyncPlay return Task.WhenAll(GetTasks()); } - /// <summary> - /// Builds a new playback command with some default values. - /// </summary> - /// <param name="type">The command type.</param> - /// <value>The SendCommand.</value> - private SendCommand NewSyncPlayCommand(SendCommandType type) + /// <inheritdoc /> + public SendCommand NewSyncPlayCommand(SendCommandType type) { return new SendCommand() { @@ -179,13 +152,8 @@ namespace Emby.Server.Implementations.SyncPlay }; } - /// <summary> - /// Builds a new group update message. - /// </summary> - /// <param name="type">The update type.</param> - /// <param name="data">The data to send.</param> - /// <value>The GroupUpdate.</value> - private GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data) + /// <inheritdoc /> + public GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data) { return new GroupUpdate<T>() { @@ -196,18 +164,44 @@ namespace Emby.Server.Implementations.SyncPlay } /// <inheritdoc /> + public string DateToUTCString(DateTime _date) + { + return _date.ToUniversalTime().ToString("o"); + } + + /// <inheritdoc /> + public long SanitizePositionTicks(long? positionTicks) + { + var ticks = positionTicks ?? 0; + ticks = ticks >= 0 ? ticks : 0; + if (_group.PlayingItem != null) + { + var runTimeTicks = _group.PlayingItem.RunTimeTicks ?? 0; + ticks = ticks > runTimeTicks ? runTimeTicks : ticks; + } + + return ticks; + } + + /// <inheritdoc /> public void CreateGroup(SessionInfo session, CancellationToken cancellationToken) { _group.AddSession(session); _syncPlayManager.AddSessionToGroup(session, this); + State = new PausedGroupState(); + _group.PlayingItem = session.FullNowPlayingItem; - _group.IsPaused = session.PlayState.IsPaused; + // TODO: looks like new groups should mantain playstate (and not force to pause) + // _group.IsPaused = session.PlayState.IsPaused; _group.PositionTicks = session.PlayState.PositionTicks ?? 0; _group.LastActivity = DateTime.UtcNow; var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); - SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); + SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); + // TODO: looks like new groups should mantain playstate (and not force to pause) + var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause); + SendCommand(session, SyncPlayBroadcastType.CurrentSession, pauseCommand, cancellationToken); } /// <inheritdoc /> @@ -219,21 +213,21 @@ namespace Emby.Server.Implementations.SyncPlay _syncPlayManager.AddSessionToGroup(session, this); var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); - SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); + SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName); - SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); + SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); // Syncing will happen client-side - if (!_group.IsPaused) + if (State.GetGroupState().Equals(GroupState.Playing)) { var playCommand = NewSyncPlayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.CurrentSession, playCommand, cancellationToken); + SendCommand(session, SyncPlayBroadcastType.CurrentSession, playCommand, cancellationToken); } else { var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken); + SendCommand(session, SyncPlayBroadcastType.CurrentSession, pauseCommand, cancellationToken); } } else @@ -244,7 +238,7 @@ namespace Emby.Server.Implementations.SyncPlay StartPositionTicks = _group.PositionTicks }; var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest); - SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken); + SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, update, cancellationToken); } } @@ -255,247 +249,21 @@ namespace Emby.Server.Implementations.SyncPlay _syncPlayManager.RemoveSessionFromGroup(session, this); var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupLeft, _group.PositionTicks); - SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); + SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserLeft, session.UserName); - SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); + SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); } /// <inheritdoc /> - public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) + public void HandleRequest(SessionInfo session, IPlaybackGroupRequest request, CancellationToken cancellationToken) { // The server's job is to maintain a consistent state for clients to reference // and notify clients of state changes. The actual syncing of media playback // happens client side. Clients are aware of the server's time and use it to sync. - switch (request.Type) - { - case PlaybackRequestType.Play: - HandlePlayRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.Pause: - HandlePauseRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.Seek: - HandleSeekRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.Buffer: - HandleBufferingRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.Ready: - HandleBufferingDoneRequest(session, request, cancellationToken); - break; - case PlaybackRequestType.Ping: - HandlePingUpdateRequest(session, request); - break; - } - } - - /// <summary> - /// Handles a play action requested by a session. - /// </summary> - /// <param name="session">The session.</param> - /// <param name="request">The play action.</param> - /// <param name="cancellationToken">The cancellation token.</param> - private void HandlePlayRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - if (_group.IsPaused) - { - // Pick a suitable time that accounts for latency - var delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing); - - // Unpause group and set starting point in future - // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position) - // The added delay does not guarantee, of course, that the command will be received in time - // Playback synchronization will mainly happen client side - _group.IsPaused = false; - _group.LastActivity = DateTime.UtcNow.AddMilliseconds( - delay); - - var command = NewSyncPlayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); - } - else - { - // Client got lost, sending current state - var command = NewSyncPlayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - } - - /// <summary> - /// Handles a pause action requested by a session. - /// </summary> - /// <param name="session">The session.</param> - /// <param name="request">The pause action.</param> - /// <param name="cancellationToken">The cancellation token.</param> - private void HandlePauseRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - if (!_group.IsPaused) - { - // Pause group and compute the media playback position - _group.IsPaused = true; - var currentTime = DateTime.UtcNow; - var elapsedTime = currentTime - _group.LastActivity; - _group.LastActivity = currentTime; - - // Seek only if playback actually started - // Pause request may be issued during the delay added to account for latency - _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; - - var command = NewSyncPlayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); - } - else - { - // Client got lost, sending current state - var command = NewSyncPlayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - } - - /// <summary> - /// Handles a seek action requested by a session. - /// </summary> - /// <param name="session">The session.</param> - /// <param name="request">The seek action.</param> - /// <param name="cancellationToken">The cancellation token.</param> - private void HandleSeekRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - // Sanitize PositionTicks - var ticks = SanitizePositionTicks(request.PositionTicks); - - // Pause and seek - _group.IsPaused = true; - _group.PositionTicks = ticks; - _group.LastActivity = DateTime.UtcNow; - - var command = NewSyncPlayCommand(SendCommandType.Seek); - SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); - } - - /// <summary> - /// Handles a buffering action requested by a session. - /// </summary> - /// <param name="session">The session.</param> - /// <param name="request">The buffering action.</param> - /// <param name="cancellationToken">The cancellation token.</param> - private void HandleBufferingRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - if (!_group.IsPaused) - { - // Pause group and compute the media playback position - _group.IsPaused = true; - var currentTime = DateTime.UtcNow; - var elapsedTime = currentTime - _group.LastActivity; - _group.LastActivity = currentTime; - _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; - - _group.SetBuffering(session, true); - - // Send pause command to all non-buffering sessions - var command = NewSyncPlayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.AllReady, command, cancellationToken); - - var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.GroupWait, session.UserName); - SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); - } - else - { - // Client got lost, sending current state - var command = NewSyncPlayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - } - - /// <summary> - /// Handles a buffering-done action requested by a session. - /// </summary> - /// <param name="session">The session.</param> - /// <param name="request">The buffering-done action.</param> - /// <param name="cancellationToken">The cancellation token.</param> - private void HandleBufferingDoneRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) - { - if (_group.IsPaused) - { - _group.SetBuffering(session, false); - - var requestTicks = SanitizePositionTicks(request.PositionTicks); - - var when = request.When ?? DateTime.UtcNow; - var currentTime = DateTime.UtcNow; - var elapsedTime = currentTime - when; - var clientPosition = TimeSpan.FromTicks(requestTicks) + elapsedTime; - var delay = _group.PositionTicks - clientPosition.Ticks; - - if (_group.IsBuffering()) - { - // Others are still buffering, tell this client to pause when ready - var command = NewSyncPlayCommand(SendCommandType.Pause); - var pauseAtTime = currentTime.AddMilliseconds(delay); - command.When = DateToUTCString(pauseAtTime); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - else - { - // Let other clients resume as soon as the buffering client catches up - _group.IsPaused = false; - - if (delay > _group.GetHighestPing() * 2) - { - // Client that was buffering is recovering, notifying others to resume - _group.LastActivity = currentTime.AddMilliseconds( - delay); - var command = NewSyncPlayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.AllExceptCurrentSession, command, cancellationToken); - } - else - { - // Client, that was buffering, resumed playback but did not update others in time - delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing); - - _group.LastActivity = currentTime.AddMilliseconds( - delay); - - var command = NewSyncPlayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.AllGroup, command, cancellationToken); - } - } - } - else - { - // Group was not waiting, make sure client has latest state - var command = NewSyncPlayCommand(SendCommandType.Play); - SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken); - } - } - - /// <summary> - /// Sanitizes the PositionTicks, considers the current playing item when available. - /// </summary> - /// <param name="positionTicks">The PositionTicks.</param> - /// <value>The sanitized PositionTicks.</value> - private long SanitizePositionTicks(long? positionTicks) - { - var ticks = positionTicks ?? 0; - ticks = ticks >= 0 ? ticks : 0; - if (_group.PlayingItem != null) - { - var runTimeTicks = _group.PlayingItem.RunTimeTicks ?? 0; - ticks = ticks > runTimeTicks ? runTimeTicks : ticks; - } - - return ticks; - } - - /// <summary> - /// Updates ping of a session. - /// </summary> - /// <param name="session">The session.</param> - /// <param name="request">The update.</param> - private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request) - { - // Collected pings are used to account for network latency when unpausing playback - _group.UpdatePing(session, request.Ping ?? GroupInfo.DefaultPing); + _logger.LogInformation("HandleRequest: {0}:{1}.", request.GetType(), State.GetGroupState()); + _ = request.Apply(this, State, session, cancellationToken); + // TODO: do something with returned value } /// <inheritdoc /> |
