Rewrite syncplay using a state design pattern

pull/3194/head
gion 4 years ago committed by Ionut Andrei Oanca
parent 5487dfc145
commit e10799e0e8

@ -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;
}
}
}

@ -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;
}
}
}

@ -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,34 +18,8 @@ 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>
@ -55,22 +30,33 @@ namespace Emby.Server.Implementations.SyncPlay
/// </summary>
private readonly ISyncPlayManager _syncPlayManager;
/// <summary>
/// The logger.
/// </summary>
private readonly ILogger _logger;
/// <summary>
/// The group to manage.
/// </summary>
private readonly GroupInfo _group = new GroupInfo();
/// <summary>
/// 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>()
{
@ -195,19 +163,45 @@ 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 />

@ -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);

@ -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);

@ -30,12 +30,6 @@ namespace MediaBrowser.Controller.SyncPlay
/// <value>The playing item.</value>
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>

@ -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);
}
}

@ -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.

@ -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.

@ -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);
}
}

@ -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);
}
}

@ -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);
}
}
}

@ -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);
}
}
}

@ -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);
}
}
}

@ -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);
}
}
}

@ -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);
}
}
}

@ -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);
}
}
}

@ -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;
}
}
}

@ -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
}
}

@ -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; }
}
}

@ -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
}
}
Loading…
Cancel
Save