aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/SyncPlay/GroupStates/PausedGroupState.cs171
-rw-r--r--Emby.Server.Implementations/SyncPlay/GroupStates/PlayingGroupState.cs94
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayController.cs416
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs4
-rw-r--r--MediaBrowser.Api/SyncPlay/SyncPlayService.cs41
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupInfo.cs6
-rw-r--r--MediaBrowser.Controller/SyncPlay/IPlaybackGroupRequest.cs24
-rw-r--r--MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/ISyncPlayState.cs95
-rw-r--r--MediaBrowser.Controller/SyncPlay/ISyncPlayStateContext.cs75
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequest/BufferGroupRequest.cs43
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequest/PauseGroupRequest.cs24
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequest/PingGroupRequest.cs31
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequest/PlayGroupRequest.cs24
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequest/ReadyGroupRequest.cs43
-rw-r--r--MediaBrowser.Controller/SyncPlay/PlaybackRequest/SeekGroupRequest.cs30
-rw-r--r--MediaBrowser.Controller/SyncPlay/SyncPlayAbstractState.cs65
-rw-r--r--MediaBrowser.Model/SyncPlay/GroupState.cs25
-rw-r--r--MediaBrowser.Model/SyncPlay/PlaybackRequest.cs34
-rw-r--r--MediaBrowser.Model/SyncPlay/SyncPlayBroadcastType.cs28
21 files changed, 891 insertions, 386 deletions
diff --git a/Emby.Server.Implementations/SyncPlay/GroupStates/PausedGroupState.cs b/Emby.Server.Implementations/SyncPlay/GroupStates/PausedGroupState.cs
new file mode 100644
index 000000000..d3bf24f74
--- /dev/null
+++ b/Emby.Server.Implementations/SyncPlay/GroupStates/PausedGroupState.cs
@@ -0,0 +1,171 @@
+using System.Linq;
+using System;
+using System.Threading;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ /// <summary>
+ /// Class PausedGroupState.
+ /// </summary>
+ /// <remarks>
+ /// Class is not thread-safe, external locking is required when accessing methods.
+ /// </remarks>
+ public class PausedGroupState : SyncPlayAbstractState
+ {
+ /// <inheritdoc />
+ public override GroupState GetGroupState()
+ {
+ return GroupState.Paused;
+ }
+
+ /// <inheritdoc />
+ public override bool HandleRequest(ISyncPlayStateContext context, bool newState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state
+ var playingState = new PlayingGroupState();
+ context.SetState(playingState);
+ return playingState.HandleRequest(context, true, request, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override bool HandleRequest(ISyncPlayStateContext context, bool newState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ if (newState)
+ {
+ GroupInfo group = context.GetGroup();
+
+ // Pause group and compute the media playback position
+ var currentTime = DateTime.UtcNow;
+ var elapsedTime = currentTime - group.LastActivity;
+ group.LastActivity = currentTime;
+ // Seek only if playback actually started
+ // Pause request may be issued during the delay added to account for latency
+ group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
+
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken);
+ }
+ else
+ {
+ // Client got lost, sending current state
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+ }
+
+ return true;
+ }
+
+ /// <inheritdoc />
+ public override bool HandleRequest(ISyncPlayStateContext context, bool newState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ GroupInfo group = context.GetGroup();
+
+ // Sanitize PositionTicks
+ var ticks = context.SanitizePositionTicks(request.PositionTicks);
+
+ // Seek
+ group.PositionTicks = ticks;
+ group.LastActivity = DateTime.UtcNow;
+
+ var command = context.NewSyncPlayCommand(SendCommandType.Seek);
+ context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken);
+
+ return true;
+ }
+
+ /// <inheritdoc />
+ public override bool HandleRequest(ISyncPlayStateContext context, bool newState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ GroupInfo group = context.GetGroup();
+
+ if (newState)
+ {
+ // Pause group and compute the media playback position
+ var currentTime = DateTime.UtcNow;
+ var elapsedTime = currentTime - group.LastActivity;
+ group.LastActivity = currentTime;
+ group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
+
+ group.SetBuffering(session, true);
+
+ // Send pause command to all non-buffering sessions
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ context.SendCommand(session, SyncPlayBroadcastType.AllReady, command, cancellationToken);
+
+ var updateOthers = context.NewSyncPlayGroupUpdate(GroupUpdateType.GroupWait, session.UserName);
+ context.SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
+ }
+ else
+ {
+ // TODO: no idea?
+ // group.SetBuffering(session, true);
+
+ // Client got lost, sending current state
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+ }
+
+ return true;
+ }
+
+ /// <inheritdoc />
+ public override bool HandleRequest(ISyncPlayStateContext context, bool newState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ GroupInfo group = context.GetGroup();
+
+ group.SetBuffering(session, false);
+
+ var requestTicks = context.SanitizePositionTicks(request.PositionTicks);
+
+ var currentTime = DateTime.UtcNow;
+ var elapsedTime = currentTime - request.When;
+ var clientPosition = TimeSpan.FromTicks(requestTicks) + elapsedTime;
+ var delay = group.PositionTicks - clientPosition.Ticks;
+
+ if (group.IsBuffering())
+ {
+ // Others are still buffering, tell this client to pause when ready
+ var command = context.NewSyncPlayCommand(SendCommandType.Pause);
+ var pauseAtTime = currentTime.AddMilliseconds(delay);
+ command.When = context.DateToUTCString(pauseAtTime);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+ }
+ else
+ {
+ // Let other clients resume as soon as the buffering client catches up
+ if (delay > group.GetHighestPing() * 2)
+ {
+ // Client that was buffering is recovering, notifying others to resume
+ group.LastActivity = currentTime.AddMilliseconds(
+ delay
+ );
+ var command = context.NewSyncPlayCommand(SendCommandType.Play);
+ context.SendCommand(session, SyncPlayBroadcastType.AllExceptCurrentSession, command, cancellationToken);
+ }
+ else
+ {
+ // Client, that was buffering, resumed playback but did not update others in time
+ delay = Math.Max(group.GetHighestPing() * 2, group.DefaultPing);
+
+ group.LastActivity = currentTime.AddMilliseconds(
+ delay
+ );
+
+ var command = context.NewSyncPlayCommand(SendCommandType.Play);
+ context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken);
+ }
+
+ // Change state
+ var playingState = new PlayingGroupState();
+ context.SetState(playingState);
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/SyncPlay/GroupStates/PlayingGroupState.cs b/Emby.Server.Implementations/SyncPlay/GroupStates/PlayingGroupState.cs
new file mode 100644
index 000000000..42c7779c1
--- /dev/null
+++ b/Emby.Server.Implementations/SyncPlay/GroupStates/PlayingGroupState.cs
@@ -0,0 +1,94 @@
+using System.Linq;
+using System;
+using System.Threading;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ /// <summary>
+ /// Class PlayingGroupState.
+ /// </summary>
+ /// <remarks>
+ /// Class is not thread-safe, external locking is required when accessing methods.
+ /// </remarks>
+ public class PlayingGroupState : SyncPlayAbstractState
+ {
+ /// <inheritdoc />
+ public override GroupState GetGroupState()
+ {
+ return GroupState.Playing;
+ }
+
+ /// <inheritdoc />
+ public override bool HandleRequest(ISyncPlayStateContext context, bool newState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ GroupInfo group = context.GetGroup();
+
+ if (newState)
+ {
+ // Pick a suitable time that accounts for latency
+ var delay = Math.Max(group.GetHighestPing() * 2, group.DefaultPing);
+
+ // Unpause group and set starting point in future
+ // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position)
+ // The added delay does not guarantee, of course, that the command will be received in time
+ // Playback synchronization will mainly happen client side
+ group.LastActivity = DateTime.UtcNow.AddMilliseconds(
+ delay
+ );
+
+ var command = context.NewSyncPlayCommand(SendCommandType.Play);
+ context.SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken);
+ }
+ else
+ {
+ // Client got lost, sending current state
+ var command = context.NewSyncPlayCommand(SendCommandType.Play);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+ }
+
+ return true;
+ }
+
+ /// <inheritdoc />
+ public override bool HandleRequest(ISyncPlayStateContext context, bool newState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state
+ var pausedState = new PausedGroupState();
+ context.SetState(pausedState);
+ return pausedState.HandleRequest(context, true, request, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override bool HandleRequest(ISyncPlayStateContext context, bool newState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state
+ var pausedState = new PausedGroupState();
+ context.SetState(pausedState);
+ return pausedState.HandleRequest(context, true, request, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override bool HandleRequest(ISyncPlayStateContext context, bool newState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Change state
+ var pausedState = new PausedGroupState();
+ context.SetState(pausedState);
+ return pausedState.HandleRequest(context, true, request, session, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public override bool HandleRequest(ISyncPlayStateContext context, bool newState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ // Group was not waiting, make sure client has latest state
+ var command = context.NewSyncPlayCommand(SendCommandType.Play);
+ context.SendCommand(session, SyncPlayBroadcastType.CurrentSession, command, cancellationToken);
+
+ return true;
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
index c98fd6d4a..225be7430 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
@@ -8,6 +8,7 @@ using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
+using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.SyncPlay
{
@@ -17,35 +18,9 @@ namespace Emby.Server.Implementations.SyncPlay
/// <remarks>
/// Class is not thread-safe, external locking is required when accessing methods.
/// </remarks>
- public class SyncPlayController : ISyncPlayController
+ public class SyncPlayController : ISyncPlayController, ISyncPlayStateContext
{
/// <summary>
- /// Used to filter the sessions of a group.
- /// </summary>
- private enum BroadcastType
- {
- /// <summary>
- /// All sessions will receive the message.
- /// </summary>
- AllGroup = 0,
-
- /// <summary>
- /// Only the specified session will receive the message.
- /// </summary>
- CurrentSession = 1,
-
- /// <summary>
- /// All sessions, except the current one, will receive the message.
- /// </summary>
- AllExceptCurrentSession = 2,
-
- /// <summary>
- /// Only sessions that are not buffering will receive the message.
- /// </summary>
- AllReady = 3
- }
-
- /// <summary>
/// The session manager.
/// </summary>
private readonly ISessionManager _sessionManager;
@@ -56,21 +31,32 @@ namespace Emby.Server.Implementations.SyncPlay
private readonly ISyncPlayManager _syncPlayManager;
/// <summary>
+ /// The logger.
+ /// </summary>
+ private readonly ILogger _logger;
+
+ /// <summary>
/// The group to manage.
/// </summary>
private readonly GroupInfo _group = new GroupInfo();
/// <summary>
- /// Initializes a new instance of the <see cref="SyncPlayController" /> class.
+ /// Internal group state.
/// </summary>
- /// <param name="sessionManager">The session manager.</param>
- /// <param name="syncPlayManager">The SyncPlay manager.</param>
- public SyncPlayController(
- ISessionManager sessionManager,
- ISyncPlayManager syncPlayManager)
+ /// <value>The group's state.</value>
+ private ISyncPlayState State = new PausedGroupState();
+
+ /// <inheritdoc />
+ public GroupInfo GetGroup()
{
- _sessionManager = sessionManager;
- _syncPlayManager = syncPlayManager;
+ return _group;
+ }
+
+ /// <inheritdoc />
+ public void SetState(ISyncPlayState state)
+ {
+ _logger.LogInformation("SetState: {0} -> {1}.", State.GetGroupState(), state.GetGroupState());
+ this.State = state;
}
/// <inheritdoc />
@@ -83,13 +69,18 @@ namespace Emby.Server.Implementations.SyncPlay
public bool IsGroupEmpty() => _group.IsEmpty();
/// <summary>
- /// Converts DateTime to UTC string.
+ /// Initializes a new instance of the <see cref="SyncPlayController" /> class.
/// </summary>
- /// <param name="date">The date to convert.</param>
- /// <value>The UTC string.</value>
- private string DateToUTCString(DateTime date)
+ /// <param name="sessionManager">The session manager.</param>
+ /// <param name="syncPlayManager">The SyncPlay manager.</param>
+ public SyncPlayController(
+ ISessionManager sessionManager,
+ ISyncPlayManager syncPlayManager,
+ ILogger logger)
{
- return date.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture);
+ _sessionManager = sessionManager;
+ _syncPlayManager = syncPlayManager;
+ _logger = logger;
}
/// <summary>
@@ -98,37 +89,30 @@ namespace Emby.Server.Implementations.SyncPlay
/// <param name="from">The current session.</param>
/// <param name="type">The filtering type.</param>
/// <value>The array of sessions matching the filter.</value>
- private IEnumerable<SessionInfo> FilterSessions(SessionInfo from, BroadcastType type)
+ private SessionInfo[] FilterSessions(SessionInfo from, SyncPlayBroadcastType type)
{
switch (type)
{
- case BroadcastType.CurrentSession:
+ case SyncPlayBroadcastType.CurrentSession:
return new SessionInfo[] { from };
- case BroadcastType.AllGroup:
- return _group.Participants.Values
- .Select(session => session.Session);
- case BroadcastType.AllExceptCurrentSession:
- return _group.Participants.Values
- .Select(session => session.Session)
- .Where(session => !session.Id.Equals(from.Id, StringComparison.Ordinal));
- case BroadcastType.AllReady:
- return _group.Participants.Values
- .Where(session => !session.IsBuffering)
- .Select(session => session.Session);
+ case SyncPlayBroadcastType.AllGroup:
+ return _group.Participants.Values.Select(
+ session => session.Session).ToArray();
+ case SyncPlayBroadcastType.AllExceptCurrentSession:
+ return _group.Participants.Values.Select(
+ session => session.Session).Where(
+ session => !session.Id.Equals(from.Id)).ToArray();
+ case SyncPlayBroadcastType.AllReady:
+ return _group.Participants.Values.Where(
+ session => !session.IsBuffering).Select(
+ session => session.Session).ToArray();
default:
return Array.Empty<SessionInfo>();
}
}
- /// <summary>
- /// Sends a GroupUpdate message to the interested sessions.
- /// </summary>
- /// <param name="from">The current session.</param>
- /// <param name="type">The filtering type.</param>
- /// <param name="message">The message to send.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <value>The task.</value>
- private Task SendGroupUpdate<T>(SessionInfo from, BroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public Task SendGroupUpdate<T>(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken)
{
IEnumerable<Task> GetTasks()
{
@@ -141,15 +125,8 @@ namespace Emby.Server.Implementations.SyncPlay
return Task.WhenAll(GetTasks());
}
- /// <summary>
- /// Sends a playback command to the interested sessions.
- /// </summary>
- /// <param name="from">The current session.</param>
- /// <param name="type">The filtering type.</param>
- /// <param name="message">The message to send.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <value>The task.</value>
- private Task SendCommand(SessionInfo from, BroadcastType type, SendCommand message, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken cancellationToken)
{
IEnumerable<Task> GetTasks()
{
@@ -162,12 +139,8 @@ namespace Emby.Server.Implementations.SyncPlay
return Task.WhenAll(GetTasks());
}
- /// <summary>
- /// Builds a new playback command with some default values.
- /// </summary>
- /// <param name="type">The command type.</param>
- /// <value>The SendCommand.</value>
- private SendCommand NewSyncPlayCommand(SendCommandType type)
+ /// <inheritdoc />
+ public SendCommand NewSyncPlayCommand(SendCommandType type)
{
return new SendCommand()
{
@@ -179,13 +152,8 @@ namespace Emby.Server.Implementations.SyncPlay
};
}
- /// <summary>
- /// Builds a new group update message.
- /// </summary>
- /// <param name="type">The update type.</param>
- /// <param name="data">The data to send.</param>
- /// <value>The GroupUpdate.</value>
- private GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data)
+ /// <inheritdoc />
+ public GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data)
{
return new GroupUpdate<T>()
{
@@ -196,18 +164,44 @@ namespace Emby.Server.Implementations.SyncPlay
}
/// <inheritdoc />
+ public string DateToUTCString(DateTime _date)
+ {
+ return _date.ToUniversalTime().ToString("o");
+ }
+
+ /// <inheritdoc />
+ public long SanitizePositionTicks(long? positionTicks)
+ {
+ var ticks = positionTicks ?? 0;
+ ticks = ticks >= 0 ? ticks : 0;
+ if (_group.PlayingItem != null)
+ {
+ var runTimeTicks = _group.PlayingItem.RunTimeTicks ?? 0;
+ ticks = ticks > runTimeTicks ? runTimeTicks : ticks;
+ }
+
+ return ticks;
+ }
+
+ /// <inheritdoc />
public void CreateGroup(SessionInfo session, CancellationToken cancellationToken)
{
_group.AddSession(session);
_syncPlayManager.AddSessionToGroup(session, this);
+ State = new PausedGroupState();
+
_group.PlayingItem = session.FullNowPlayingItem;
- _group.IsPaused = session.PlayState.IsPaused;
+ // TODO: looks like new groups should mantain playstate (and not force to pause)
+ // _group.IsPaused = session.PlayState.IsPaused;
_group.PositionTicks = session.PlayState.PositionTicks ?? 0;
_group.LastActivity = DateTime.UtcNow;
var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow));
- SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
+ SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
+ // TODO: looks like new groups should mantain playstate (and not force to pause)
+ var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause);
+ SendCommand(session, SyncPlayBroadcastType.CurrentSession, pauseCommand, cancellationToken);
}
/// <inheritdoc />
@@ -219,21 +213,21 @@ namespace Emby.Server.Implementations.SyncPlay
_syncPlayManager.AddSessionToGroup(session, this);
var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow));
- SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
+ SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
- SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
+ SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
// Syncing will happen client-side
- if (!_group.IsPaused)
+ if (State.GetGroupState().Equals(GroupState.Playing))
{
var playCommand = NewSyncPlayCommand(SendCommandType.Play);
- SendCommand(session, BroadcastType.CurrentSession, playCommand, cancellationToken);
+ SendCommand(session, SyncPlayBroadcastType.CurrentSession, playCommand, cancellationToken);
}
else
{
var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause);
- SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken);
+ SendCommand(session, SyncPlayBroadcastType.CurrentSession, pauseCommand, cancellationToken);
}
}
else
@@ -244,7 +238,7 @@ namespace Emby.Server.Implementations.SyncPlay
StartPositionTicks = _group.PositionTicks
};
var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
- SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken);
+ SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, update, cancellationToken);
}
}
@@ -255,247 +249,21 @@ namespace Emby.Server.Implementations.SyncPlay
_syncPlayManager.RemoveSessionFromGroup(session, this);
var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupLeft, _group.PositionTicks);
- SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
+ SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserLeft, session.UserName);
- SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
+ SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
}
/// <inheritdoc />
- public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
+ public void HandleRequest(SessionInfo session, IPlaybackGroupRequest request, CancellationToken cancellationToken)
{
// The server's job is to maintain a consistent state for clients to reference
// and notify clients of state changes. The actual syncing of media playback
// happens client side. Clients are aware of the server's time and use it to sync.
- switch (request.Type)
- {
- case PlaybackRequestType.Play:
- HandlePlayRequest(session, request, cancellationToken);
- break;
- case PlaybackRequestType.Pause:
- HandlePauseRequest(session, request, cancellationToken);
- break;
- case PlaybackRequestType.Seek:
- HandleSeekRequest(session, request, cancellationToken);
- break;
- case PlaybackRequestType.Buffer:
- HandleBufferingRequest(session, request, cancellationToken);
- break;
- case PlaybackRequestType.Ready:
- HandleBufferingDoneRequest(session, request, cancellationToken);
- break;
- case PlaybackRequestType.Ping:
- HandlePingUpdateRequest(session, request);
- break;
- }
- }
-
- /// <summary>
- /// Handles a play action requested by a session.
- /// </summary>
- /// <param name="session">The session.</param>
- /// <param name="request">The play action.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- private void HandlePlayRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
- {
- if (_group.IsPaused)
- {
- // Pick a suitable time that accounts for latency
- var delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing);
-
- // Unpause group and set starting point in future
- // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position)
- // The added delay does not guarantee, of course, that the command will be received in time
- // Playback synchronization will mainly happen client side
- _group.IsPaused = false;
- _group.LastActivity = DateTime.UtcNow.AddMilliseconds(
- delay);
-
- var command = NewSyncPlayCommand(SendCommandType.Play);
- SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
- }
- else
- {
- // Client got lost, sending current state
- var command = NewSyncPlayCommand(SendCommandType.Play);
- SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
- }
- }
-
- /// <summary>
- /// Handles a pause action requested by a session.
- /// </summary>
- /// <param name="session">The session.</param>
- /// <param name="request">The pause action.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- private void HandlePauseRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
- {
- if (!_group.IsPaused)
- {
- // Pause group and compute the media playback position
- _group.IsPaused = true;
- var currentTime = DateTime.UtcNow;
- var elapsedTime = currentTime - _group.LastActivity;
- _group.LastActivity = currentTime;
-
- // Seek only if playback actually started
- // Pause request may be issued during the delay added to account for latency
- _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
-
- var command = NewSyncPlayCommand(SendCommandType.Pause);
- SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
- }
- else
- {
- // Client got lost, sending current state
- var command = NewSyncPlayCommand(SendCommandType.Pause);
- SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
- }
- }
-
- /// <summary>
- /// Handles a seek action requested by a session.
- /// </summary>
- /// <param name="session">The session.</param>
- /// <param name="request">The seek action.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- private void HandleSeekRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
- {
- // Sanitize PositionTicks
- var ticks = SanitizePositionTicks(request.PositionTicks);
-
- // Pause and seek
- _group.IsPaused = true;
- _group.PositionTicks = ticks;
- _group.LastActivity = DateTime.UtcNow;
-
- var command = NewSyncPlayCommand(SendCommandType.Seek);
- SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
- }
-
- /// <summary>
- /// Handles a buffering action requested by a session.
- /// </summary>
- /// <param name="session">The session.</param>
- /// <param name="request">The buffering action.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- private void HandleBufferingRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
- {
- if (!_group.IsPaused)
- {
- // Pause group and compute the media playback position
- _group.IsPaused = true;
- var currentTime = DateTime.UtcNow;
- var elapsedTime = currentTime - _group.LastActivity;
- _group.LastActivity = currentTime;
- _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
-
- _group.SetBuffering(session, true);
-
- // Send pause command to all non-buffering sessions
- var command = NewSyncPlayCommand(SendCommandType.Pause);
- SendCommand(session, BroadcastType.AllReady, command, cancellationToken);
-
- var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.GroupWait, session.UserName);
- SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
- }
- else
- {
- // Client got lost, sending current state
- var command = NewSyncPlayCommand(SendCommandType.Pause);
- SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
- }
- }
-
- /// <summary>
- /// Handles a buffering-done action requested by a session.
- /// </summary>
- /// <param name="session">The session.</param>
- /// <param name="request">The buffering-done action.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- private void HandleBufferingDoneRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
- {
- if (_group.IsPaused)
- {
- _group.SetBuffering(session, false);
-
- var requestTicks = SanitizePositionTicks(request.PositionTicks);
-
- var when = request.When ?? DateTime.UtcNow;
- var currentTime = DateTime.UtcNow;
- var elapsedTime = currentTime - when;
- var clientPosition = TimeSpan.FromTicks(requestTicks) + elapsedTime;
- var delay = _group.PositionTicks - clientPosition.Ticks;
-
- if (_group.IsBuffering())
- {
- // Others are still buffering, tell this client to pause when ready
- var command = NewSyncPlayCommand(SendCommandType.Pause);
- var pauseAtTime = currentTime.AddMilliseconds(delay);
- command.When = DateToUTCString(pauseAtTime);
- SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
- }
- else
- {
- // Let other clients resume as soon as the buffering client catches up
- _group.IsPaused = false;
-
- if (delay > _group.GetHighestPing() * 2)
- {
- // Client that was buffering is recovering, notifying others to resume
- _group.LastActivity = currentTime.AddMilliseconds(
- delay);
- var command = NewSyncPlayCommand(SendCommandType.Play);
- SendCommand(session, BroadcastType.AllExceptCurrentSession, command, cancellationToken);
- }
- else
- {
- // Client, that was buffering, resumed playback but did not update others in time
- delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing);
-
- _group.LastActivity = currentTime.AddMilliseconds(
- delay);
-
- var command = NewSyncPlayCommand(SendCommandType.Play);
- SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
- }
- }
- }
- else
- {
- // Group was not waiting, make sure client has latest state
- var command = NewSyncPlayCommand(SendCommandType.Play);
- SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
- }
- }
-
- /// <summary>
- /// Sanitizes the PositionTicks, considers the current playing item when available.
- /// </summary>
- /// <param name="positionTicks">The PositionTicks.</param>
- /// <value>The sanitized PositionTicks.</value>
- private long SanitizePositionTicks(long? positionTicks)
- {
- var ticks = positionTicks ?? 0;
- ticks = ticks >= 0 ? ticks : 0;
- if (_group.PlayingItem != null)
- {
- var runTimeTicks = _group.PlayingItem.RunTimeTicks ?? 0;
- ticks = ticks > runTimeTicks ? runTimeTicks : ticks;
- }
-
- return ticks;
- }
-
- /// <summary>
- /// Updates ping of a session.
- /// </summary>
- /// <param name="session">The session.</param>
- /// <param name="request">The update.</param>
- private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request)
- {
- // Collected pings are used to account for network latency when unpausing playback
- _group.UpdatePing(session, request.Ping ?? GroupInfo.DefaultPing);
+ _logger.LogInformation("HandleRequest: {0}:{1}.", request.GetType(), State.GetGroupState());
+ _ = request.Apply(this, State, session, cancellationToken);
+ // TODO: do something with returned value
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
index 550939d70..b85f3c149 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
@@ -186,7 +186,7 @@ namespace Emby.Server.Implementations.SyncPlay
LeaveGroup(session, cancellationToken);
}
- var group = new SyncPlayController(_sessionManager, this);
+ var group = new SyncPlayController(_sessionManager, this, _logger);
_groups[group.GetGroupId()] = group;
group.CreateGroup(session, cancellationToken);
@@ -312,7 +312,7 @@ namespace Emby.Server.Implementations.SyncPlay
}
/// <inheritdoc />
- public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
+ public void HandleRequest(SessionInfo session, IPlaybackGroupRequest request, CancellationToken cancellationToken)
{
var user = _userManager.GetUserById(session.UserId);
diff --git a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs
index 6035f84e4..bd9670f07 100644
--- a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs
+++ b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs
@@ -172,7 +172,7 @@ namespace MediaBrowser.Api.SyncPlay
/// </summary>
/// <param name="request">The request.</param>
/// <value>The requested list of groups.</value>
- public List<GroupInfoDto> Get(SyncPlayListGroups request)
+ public List<GroupInfoDto> Get(SyncPlayList request)
{
var currentSession = GetSession(_sessionContext);
var filterItemId = Guid.Empty;
@@ -192,10 +192,7 @@ namespace MediaBrowser.Api.SyncPlay
public void Post(SyncPlayPlay request)
{
var currentSession = GetSession(_sessionContext);
- var syncPlayRequest = new PlaybackRequest()
- {
- Type = PlaybackRequestType.Play
- };
+ var syncPlayRequest = new PlayGroupRequest();
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
}
@@ -206,10 +203,7 @@ namespace MediaBrowser.Api.SyncPlay
public void Post(SyncPlayPause request)
{
var currentSession = GetSession(_sessionContext);
- var syncPlayRequest = new PlaybackRequest()
- {
- Type = PlaybackRequestType.Pause
- };
+ var syncPlayRequest = new PauseGroupRequest();
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
}
@@ -220,9 +214,8 @@ namespace MediaBrowser.Api.SyncPlay
public void Post(SyncPlaySeek request)
{
var currentSession = GetSession(_sessionContext);
- var syncPlayRequest = new PlaybackRequest()
+ var syncPlayRequest = new SeekGroupRequest()
{
- Type = PlaybackRequestType.Seek,
PositionTicks = request.PositionTicks
};
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
@@ -235,12 +228,25 @@ namespace MediaBrowser.Api.SyncPlay
public void Post(SyncPlayBuffering request)
{
var currentSession = GetSession(_sessionContext);
- var syncPlayRequest = new PlaybackRequest()
+
+ IPlaybackGroupRequest syncPlayRequest;
+ if (!request.BufferingDone)
{
- Type = request.BufferingDone ? PlaybackRequestType.Ready : PlaybackRequestType.Buffer,
- When = DateTime.Parse(request.When),
- PositionTicks = request.PositionTicks
- };
+ syncPlayRequest = new BufferGroupRequest()
+ {
+ When = DateTime.Parse(request.When),
+ PositionTicks = request.PositionTicks
+ };
+ }
+ else
+ {
+ syncPlayRequest = new ReadyGroupRequest()
+ {
+ When = DateTime.Parse(request.When),
+ PositionTicks = request.PositionTicks
+ };
+ }
+
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
}
@@ -251,9 +257,8 @@ namespace MediaBrowser.Api.SyncPlay
public void Post(SyncPlayPing request)
{
var currentSession = GetSession(_sessionContext);
- var syncPlayRequest = new PlaybackRequest()
+ var syncPlayRequest = new PingGroupRequest()
{
- Type = PlaybackRequestType.Ping,
Ping = Convert.ToInt64(request.Ping)
};
_syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);
diff --git a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
index a1cada25c..cdd24d0b5 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
@@ -31,12 +31,6 @@ namespace MediaBrowser.Controller.SyncPlay
public BaseItem PlayingItem { get; set; }
/// <summary>
- /// Gets or sets a value indicating whether playback is paused.
- /// </summary>
- /// <value>Playback is paused.</value>
- public bool IsPaused { get; set; }
-
- /// <summary>
/// Gets or sets a value indicating whether there are position ticks.
/// </summary>
/// <value>The position ticks.</value>
diff --git a/MediaBrowser.Controller/SyncPlay/IPlaybackGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/IPlaybackGroupRequest.cs
new file mode 100644
index 000000000..a6e87a007
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/IPlaybackGroupRequest.cs
@@ -0,0 +1,24 @@
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ /// <summary>
+ /// Interface IPlaybackGroupRequest.
+ /// </summary>
+ public interface IPlaybackGroupRequest
+ {
+ /// <summary>
+ /// Gets the playback request type.
+ /// </summary>
+ /// <value>The playback request type.</value>
+ PlaybackRequestType Type();
+
+ /// <summary>
+ /// Applies the request to a group.
+ /// </summary>
+ /// <value>The operation completion status.</value>
+ bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs
index d869c05bd..5ac2aeb24 100644
--- a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs
+++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs
@@ -56,7 +56,7 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="session">The session.</param>
/// <param name="request">The requested action.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken);
+ void HandleRequest(SessionInfo session, IPlaybackGroupRequest request, CancellationToken cancellationToken);
/// <summary>
/// Gets the info about the group for the clients.
diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
index 65770021d..6fa94e2ce 100644
--- a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
+++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs
@@ -48,7 +48,7 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="session">The session.</param>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
- void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken);
+ void HandleRequest(SessionInfo session, IPlaybackGroupRequest request, CancellationToken cancellationToken);
/// <summary>
/// Maps a session to a group.
diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayState.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayState.cs
new file mode 100644
index 000000000..55c9ee938
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayState.cs
@@ -0,0 +1,95 @@
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ /// <summary>
+ /// Interface ISyncPlayState.
+ /// </summary>
+ public interface ISyncPlayState
+ {
+ /// <summary>
+ /// Gets the group state.
+ /// </summary>
+ /// <value>The group state.</value>
+ GroupState GetGroupState();
+
+ /// <summary>
+ /// Generic handle. Context's state can change.
+ /// </summary>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="newState">Whether the state has been just set.</param>
+ /// <param name="request">The play action.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <value>The operation completion status.</value>
+ bool HandleRequest(ISyncPlayStateContext context, bool newState, IPlaybackGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a play action requested by a session. Context's state can change.
+ /// </summary>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="newState">Whether the state has been just set.</param>
+ /// <param name="request">The play action.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <value>The operation completion status.</value>
+ bool HandleRequest(ISyncPlayStateContext context, bool newState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a pause action requested by a session. Context's state can change.
+ /// </summary>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="newState">Whether the state has been just set.</param>
+ /// <param name="request">The pause action.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <value>The operation completion status.</value>
+ bool HandleRequest(ISyncPlayStateContext context, bool newState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a seek action requested by a session. Context's state can change.
+ /// </summary>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="newState">Whether the state has been just set.</param>
+ /// <param name="request">The seek action.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <value>The operation completion status.</value>
+ bool HandleRequest(ISyncPlayStateContext context, bool newState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a buffering action requested by a session. Context's state can change.
+ /// </summary>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="newState">Whether the state has been just set.</param>
+ /// <param name="request">The buffering action.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <value>The operation completion status.</value>
+ bool HandleRequest(ISyncPlayStateContext context, bool newState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Handles a buffering-done action requested by a session. Context's state can change.
+ /// </summary>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="newState">Whether the state has been just set.</param>
+ /// <param name="request">The buffering-done action.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <value>The operation completion status.</value>
+ bool HandleRequest(ISyncPlayStateContext context, bool newState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Updates ping of a session. Context's state should not change.
+ /// </summary>
+ /// <param name="context">The context of the state.</param>
+ /// <param name="newState">Whether the state has been just set.</param>
+ /// <param name="request">The buffering-done action.</param>
+ /// <param name="session">The session.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <value>The operation completion status.</value>
+ bool HandleRequest(ISyncPlayStateContext context, bool newState, PingGroupRequest request, SessionInfo session, CancellationToken cancellationToken);
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayStateContext.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayStateContext.cs
new file mode 100644
index 000000000..9bdb1ace6
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayStateContext.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ /// <summary>
+ /// Interface ISyncPlayStateContext.
+ /// </summary>
+ public interface ISyncPlayStateContext
+ {
+ /// <summary>
+ /// Gets the context's group.
+ /// </summary>
+ /// <value>The group.</value>
+ GroupInfo GetGroup();
+
+ /// <summary>
+ /// Sets a new state.
+ /// </summary>
+ /// <param name="state">The new state.</param>
+ void SetState(ISyncPlayState state);
+
+ /// <summary>
+ /// Sends a GroupUpdate message to the interested sessions.
+ /// </summary>
+ /// <param name="from">The current session.</param>
+ /// <param name="type">The filtering type.</param>
+ /// <param name="message">The message to send.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <value>The task.</value>
+ Task SendGroupUpdate<T>(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Sends a playback command to the interested sessions.
+ /// </summary>
+ /// <param name="from">The current session.</param>
+ /// <param name="type">The filtering type.</param>
+ /// <param name="message">The message to send.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <value>The task.</value>
+ Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Builds a new playback command with some default values.
+ /// </summary>
+ /// <param name="type">The command type.</param>
+ /// <value>The SendCommand.</value>
+ SendCommand NewSyncPlayCommand(SendCommandType type);
+
+ /// <summary>
+ /// Builds a new group update message.
+ /// </summary>
+ /// <param name="type">The update type.</param>
+ /// <param name="data">The data to send.</param>
+ /// <value>The GroupUpdate.</value>
+ GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data);
+
+ /// <summary>
+ /// Converts DateTime to UTC string.
+ /// </summary>
+ /// <param name="date">The date to convert.</param>
+ /// <value>The UTC string.</value>
+ string DateToUTCString(DateTime date);
+
+ /// <summary>
+ /// Sanitizes the PositionTicks, considers the current playing item when available.
+ /// </summary>
+ /// <param name="positionTicks">The PositionTicks.</param>
+ /// <value>The sanitized PositionTicks.</value>
+ long SanitizePositionTicks(long? positionTicks);
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/BufferGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/BufferGroupRequest.cs
new file mode 100644
index 000000000..21dae8e4e
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/BufferGroupRequest.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ /// <summary>
+ /// Class BufferingGroupRequest.
+ /// </summary>
+ public class BufferGroupRequest : IPlaybackGroupRequest
+ {
+ /// <summary>
+ /// Gets or sets when the request has been made by the client.
+ /// </summary>
+ /// <value>The date of the request.</value>
+ public DateTime When { get; set; }
+
+ /// <summary>
+ /// Gets or sets the position ticks.
+ /// </summary>
+ /// <value>The position ticks.</value>
+ public long PositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playing item id.
+ /// </summary>
+ /// <value>The playing item id.</value>
+ public Guid PlayingItemId { get; set; }
+
+ /// <inheritdoc />
+ public PlaybackRequestType Type()
+ {
+ return PlaybackRequestType.Buffer;
+ }
+
+ /// <inheritdoc />
+ public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ return state.HandleRequest(context, false, this, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PauseGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PauseGroupRequest.cs
new file mode 100644
index 000000000..21a46add8
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PauseGroupRequest.cs
@@ -0,0 +1,24 @@
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ /// <summary>
+ /// Class PauseGroupRequest.
+ /// </summary>
+ public class PauseGroupRequest : IPlaybackGroupRequest
+ {
+ /// <inheritdoc />
+ public PlaybackRequestType Type()
+ {
+ return PlaybackRequestType.Pause;
+ }
+
+ /// <inheritdoc />
+ public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ return state.HandleRequest(context, false, this, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PingGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PingGroupRequest.cs
new file mode 100644
index 000000000..2f78edfc5
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PingGroupRequest.cs
@@ -0,0 +1,31 @@
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+// FIXME: not really group related, can be moved up to SyncPlayController maybe?
+namespace MediaBrowser.Controller.SyncPlay
+{
+ /// <summary>
+ /// Class UpdatePingGroupRequest.
+ /// </summary>
+ public class PingGroupRequest : IPlaybackGroupRequest
+ {
+ /// <summary>
+ /// Gets or sets the ping time.
+ /// </summary>
+ /// <value>The ping time.</value>
+ public long Ping { get; set; }
+
+ /// <inheritdoc />
+ public PlaybackRequestType Type()
+ {
+ return PlaybackRequestType.Ping;
+ }
+
+ /// <inheritdoc />
+ public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ return state.HandleRequest(context, false, this, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PlayGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PlayGroupRequest.cs
new file mode 100644
index 000000000..942229a77
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/PlayGroupRequest.cs
@@ -0,0 +1,24 @@
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ /// <summary>
+ /// Class PlayGroupRequest.
+ /// </summary>
+ public class PlayGroupRequest : IPlaybackGroupRequest
+ {
+ /// <inheritdoc />
+ public PlaybackRequestType Type()
+ {
+ return PlaybackRequestType.Play;
+ }
+
+ /// <inheritdoc />
+ public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ return state.HandleRequest(context, false, this, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/ReadyGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/ReadyGroupRequest.cs
new file mode 100644
index 000000000..ee88ddddb
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/ReadyGroupRequest.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ /// <summary>
+ /// Class BufferingDoneGroupRequest.
+ /// </summary>
+ public class ReadyGroupRequest : IPlaybackGroupRequest
+ {
+ /// <summary>
+ /// Gets or sets when the request has been made by the client.
+ /// </summary>
+ /// <value>The date of the request.</value>
+ public DateTime When { get; set; }
+
+ /// <summary>
+ /// Gets or sets the position ticks.
+ /// </summary>
+ /// <value>The position ticks.</value>
+ public long PositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playing item id.
+ /// </summary>
+ /// <value>The playing item id.</value>
+ public Guid PlayingItemId { get; set; }
+
+ /// <inheritdoc />
+ public PlaybackRequestType Type()
+ {
+ return PlaybackRequestType.Ready;
+ }
+
+ /// <inheritdoc />
+ public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ return state.HandleRequest(context, false, this, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SeekGroupRequest.cs b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SeekGroupRequest.cs
new file mode 100644
index 000000000..bb5e7a343
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/PlaybackRequest/SeekGroupRequest.cs
@@ -0,0 +1,30 @@
+using System.Threading;
+using MediaBrowser.Model.SyncPlay;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ /// <summary>
+ /// Class SeekGroupRequest.
+ /// </summary>
+ public class SeekGroupRequest : IPlaybackGroupRequest
+ {
+ /// <summary>
+ /// Gets or sets the position ticks.
+ /// </summary>
+ /// <value>The position ticks.</value>
+ public long PositionTicks { get; set; }
+
+ /// <inheritdoc />
+ public PlaybackRequestType Type()
+ {
+ return PlaybackRequestType.Seek;
+ }
+
+ /// <inheritdoc />
+ public bool Apply(ISyncPlayStateContext context, ISyncPlayState state, SessionInfo session, CancellationToken cancellationToken)
+ {
+ return state.HandleRequest(context, false, this, session, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/SyncPlay/SyncPlayAbstractState.cs b/MediaBrowser.Controller/SyncPlay/SyncPlayAbstractState.cs
new file mode 100644
index 000000000..0b72d1668
--- /dev/null
+++ b/MediaBrowser.Controller/SyncPlay/SyncPlayAbstractState.cs
@@ -0,0 +1,65 @@
+using System.Threading;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.SyncPlay
+{
+ /// <summary>
+ /// Class SyncPlayAbstractState.
+ /// </summary>
+ /// <remarks>
+ /// Class is not thread-safe, external locking is required when accessing methods.
+ /// </remarks>
+ public abstract class SyncPlayAbstractState : ISyncPlayState
+ {
+ /// <inheritdoc />
+ public abstract GroupState GetGroupState();
+
+ /// <inheritdoc />
+ public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, IPlaybackGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ return true;
+ }
+
+ /// <inheritdoc />
+ public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, PlayGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ return true;
+ }
+
+ /// <inheritdoc />
+ public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, PauseGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ return true;
+ }
+
+ /// <inheritdoc />
+ public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, SeekGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ return true;
+ }
+
+ /// <inheritdoc />
+ public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, BufferGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ return true;
+ }
+
+ /// <inheritdoc />
+ public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, ReadyGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ return true;
+ }
+
+ /// <inheritdoc />
+ public virtual bool HandleRequest(ISyncPlayStateContext context, bool newState, PingGroupRequest request, SessionInfo session, CancellationToken cancellationToken)
+ {
+ GroupInfo group = context.GetGroup();
+
+ // Collected pings are used to account for network latency when unpausing playback
+ group.UpdatePing(session, request.Ping);
+
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Model/SyncPlay/GroupState.cs b/MediaBrowser.Model/SyncPlay/GroupState.cs
new file mode 100644
index 000000000..871634d55
--- /dev/null
+++ b/MediaBrowser.Model/SyncPlay/GroupState.cs
@@ -0,0 +1,25 @@
+namespace MediaBrowser.Model.SyncPlay
+{
+ /// <summary>
+ /// Enum GroupState.
+ /// </summary>
+ public enum GroupState
+ {
+ /// <summary>
+ /// The group is in idle state. No media is playing.
+ /// </summary>
+ Idle,
+ /// <summary>
+ /// The group is in wating state. Playback is paused. Will start playing when users are ready.
+ /// </summary>
+ Waiting,
+ /// <summary>
+ /// The group is in paused state. Playback is paused. Will resume on play command.
+ /// </summary>
+ Paused,
+ /// <summary>
+ /// The group is in playing state. Playback is advancing.
+ /// </summary>
+ Playing
+ }
+}
diff --git a/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs b/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs
deleted file mode 100644
index 9de23194e..000000000
--- a/MediaBrowser.Model/SyncPlay/PlaybackRequest.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System;
-
-namespace MediaBrowser.Model.SyncPlay
-{
- /// <summary>
- /// Class PlaybackRequest.
- /// </summary>
- public class PlaybackRequest
- {
- /// <summary>
- /// Gets or sets the request type.
- /// </summary>
- /// <value>The request type.</value>
- public PlaybackRequestType Type { get; set; }
-
- /// <summary>
- /// Gets or sets when the request has been made by the client.
- /// </summary>
- /// <value>The date of the request.</value>
- public DateTime? When { get; set; }
-
- /// <summary>
- /// Gets or sets the position ticks.
- /// </summary>
- /// <value>The position ticks.</value>
- public long? PositionTicks { get; set; }
-
- /// <summary>
- /// Gets or sets the ping time.
- /// </summary>
- /// <value>The ping time.</value>
- public long? Ping { get; set; }
- }
-}
diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayBroadcastType.cs b/MediaBrowser.Model/SyncPlay/SyncPlayBroadcastType.cs
new file mode 100644
index 000000000..29dbb11b3
--- /dev/null
+++ b/MediaBrowser.Model/SyncPlay/SyncPlayBroadcastType.cs
@@ -0,0 +1,28 @@
+namespace MediaBrowser.Model.SyncPlay
+{
+ /// <summary>
+ /// Used to filter the sessions of a group.
+ /// </summary>
+ public enum SyncPlayBroadcastType
+ {
+ /// <summary>
+ /// All sessions will receive the message.
+ /// </summary>
+ AllGroup = 0,
+
+ /// <summary>
+ /// Only the specified session will receive the message.
+ /// </summary>
+ CurrentSession = 1,
+
+ /// <summary>
+ /// All sessions, except the current one, will receive the message.
+ /// </summary>
+ AllExceptCurrentSession = 2,
+
+ /// <summary>
+ /// Only sessions that are not buffering will receive the message.
+ /// </summary>
+ AllReady = 3
+ }
+}