2020-09-24 17:04:21 -04:00
using System ;
using System.Collections.Generic ;
using System.Globalization ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
using Jellyfin.Data.Entities ;
using Jellyfin.Data.Enums ;
using MediaBrowser.Controller.Entities ;
using MediaBrowser.Controller.Library ;
using MediaBrowser.Controller.Session ;
using MediaBrowser.Controller.SyncPlay ;
2020-11-14 17:40:01 -05:00
using MediaBrowser.Controller.SyncPlay.GroupStates ;
using MediaBrowser.Controller.SyncPlay.Queue ;
2020-09-24 17:04:21 -04:00
using MediaBrowser.Model.SyncPlay ;
using Microsoft.Extensions.Logging ;
namespace Emby.Server.Implementations.SyncPlay
{
/// <summary>
2020-11-13 09:13:32 -05:00
/// Class GroupController.
2020-09-24 17:04:21 -04:00
/// </summary>
/// <remarks>
/// Class is not thread-safe, external locking is required when accessing methods.
/// </remarks>
2020-11-13 09:13:32 -05:00
public class GroupController : IGroupController , IGroupStateContext
2020-09-24 17:04:21 -04:00
{
/// <summary>
/// The logger.
/// </summary>
private readonly ILogger _logger ;
/// <summary>
/// The user manager.
/// </summary>
private readonly IUserManager _userManager ;
/// <summary>
/// The session manager.
/// </summary>
private readonly ISessionManager _sessionManager ;
/// <summary>
/// The library manager.
/// </summary>
private readonly ILibraryManager _libraryManager ;
/// <summary>
/// Internal group state.
/// </summary>
/// <value>The group's state.</value>
2020-11-13 09:13:32 -05:00
private IGroupState _state ;
/// <summary>
/// Initializes a new instance of the <see cref="GroupController" /> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="userManager">The user manager.</param>
/// <param name="sessionManager">The session manager.</param>
/// <param name="libraryManager">The library manager.</param>
public GroupController (
ILogger logger ,
IUserManager userManager ,
ISessionManager sessionManager ,
2020-11-15 11:03:27 -05:00
ILibraryManager libraryManager )
2020-11-13 09:13:32 -05:00
{
_logger = logger ;
_userManager = userManager ;
_sessionManager = sessionManager ;
_libraryManager = libraryManager ;
_state = new IdleGroupState ( _logger ) ;
}
/// <summary>
/// Gets the default ping value used for sessions.
/// </summary>
public long DefaultPing { get ; } = 500 ;
/// <summary>
/// Gets the maximum time offset error accepted for dates reported by clients, in milliseconds.
/// </summary>
public long TimeSyncOffset { get ; } = 2000 ;
/// <summary>
/// Gets the maximum offset error accepted for position reported by clients, in milliseconds.
/// </summary>
public long MaxPlaybackOffset { get ; } = 500 ;
2020-09-24 17:04:21 -04:00
/// <summary>
/// Gets the group identifier.
/// </summary>
/// <value>The group identifier.</value>
public Guid GroupId { get ; } = Guid . NewGuid ( ) ;
/// <summary>
/// Gets the group name.
/// </summary>
/// <value>The group name.</value>
public string GroupName { get ; private set ; }
/// <summary>
/// Gets the group identifier.
/// </summary>
/// <value>The group identifier.</value>
public PlayQueueManager PlayQueue { get ; } = new PlayQueueManager ( ) ;
/// <summary>
2020-11-13 09:13:32 -05:00
/// Gets the runtime ticks of current playing item.
2020-09-24 17:04:21 -04:00
/// </summary>
/// <value>The runtime ticks of current playing item.</value>
public long RunTimeTicks { get ; private set ; }
/// <summary>
/// Gets or sets the position ticks.
/// </summary>
/// <value>The position ticks.</value>
public long PositionTicks { get ; set ; }
/// <summary>
/// Gets or sets the last activity.
/// </summary>
/// <value>The last activity.</value>
public DateTime LastActivity { get ; set ; }
/// <summary>
/// Gets the participants.
/// </summary>
/// <value>The participants, or members of the group.</value>
public Dictionary < string , GroupMember > Participants { get ; } =
new Dictionary < string , GroupMember > ( StringComparer . OrdinalIgnoreCase ) ;
/// <summary>
/// Adds the session to the group.
/// </summary>
/// <param name="session">The session.</param>
private void AddSession ( SessionInfo session )
{
Participants . TryAdd (
session . Id ,
new GroupMember
{
Session = session ,
Ping = DefaultPing ,
IsBuffering = false
} ) ;
}
/// <summary>
/// Removes the session from the group.
/// </summary>
/// <param name="session">The session.</param>
private void RemoveSession ( SessionInfo session )
{
Participants . Remove ( session . Id ) ;
}
/// <summary>
/// Filters sessions of this group.
/// </summary>
/// <param name="from">The current session.</param>
/// <param name="type">The filtering type.</param>
2020-11-15 11:03:27 -05:00
/// <returns>The list of sessions matching the filter.</returns>
2020-11-13 09:13:32 -05:00
private IEnumerable < SessionInfo > FilterSessions ( SessionInfo from , SyncPlayBroadcastType type )
{
return type switch
{
SyncPlayBroadcastType . CurrentSession = > new SessionInfo [ ] { from } ,
SyncPlayBroadcastType . AllGroup = > Participants
. Values
. Select ( session = > session . Session ) ,
SyncPlayBroadcastType . AllExceptCurrentSession = > Participants
. Values
. Select ( session = > session . Session )
. Where ( session = > ! session . Id . Equals ( from . Id , StringComparison . OrdinalIgnoreCase ) ) ,
SyncPlayBroadcastType . AllReady = > Participants
. Values
. Where ( session = > ! session . IsBuffering )
. Select ( session = > session . Session ) ,
_ = > Enumerable . Empty < SessionInfo > ( )
} ;
2020-09-24 17:04:21 -04:00
}
2020-11-13 09:13:32 -05:00
/// <summary>
/// Checks if a given user can access a given item, that is, the user has access to a folder where the item is stored.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="item">The item.</param>
/// <returns><c>true</c> if the user can access the item, <c>false</c> otherwise.</returns>
2020-09-24 17:04:21 -04:00
private bool HasAccessToItem ( User user , BaseItem item )
{
var collections = _libraryManager . GetCollectionFolders ( item )
. Select ( folder = > folder . Id . ToString ( "N" , CultureInfo . InvariantCulture ) ) ;
return collections . Intersect ( user . GetPreference ( PreferenceKind . EnabledFolders ) ) . Any ( ) ;
}
2020-11-13 09:13:32 -05:00
/// <summary>
/// Checks if a given user can access all items of a given queue, that is,
/// the user has the required minimum parental access and has access to all required folders.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="queue">The queue.</param>
/// <returns><c>true</c> if the user can access all the items in the queue, <c>false</c> otherwise.</returns>
2020-11-15 11:03:27 -05:00
private bool HasAccessToQueue ( User user , IReadOnlyList < Guid > queue )
2020-09-24 17:04:21 -04:00
{
2020-11-13 09:13:32 -05:00
// Check if queue is empty.
if ( ! queue ? . Any ( ) ? ? true )
2020-09-24 17:04:21 -04:00
{
return true ;
}
2020-11-13 09:13:32 -05:00
foreach ( var itemId in queue )
2020-09-24 17:04:21 -04:00
{
2020-11-13 09:13:32 -05:00
var item = _libraryManager . GetItemById ( itemId ) ;
if ( user . MaxParentalAgeRating . HasValue & & item . InheritedParentalRatingValue > user . MaxParentalAgeRating )
{
return false ;
}
2020-09-24 17:04:21 -04:00
2020-11-13 09:13:32 -05:00
if ( ! user . HasPermission ( PermissionKind . EnableAllFolders ) & & ! HasAccessToItem ( user , item ) )
{
return false ;
}
2020-09-24 17:04:21 -04:00
}
2020-11-13 09:13:32 -05:00
return true ;
2020-09-24 17:04:21 -04:00
}
2020-11-15 11:03:27 -05:00
private bool AllUsersHaveAccessToQueue ( IReadOnlyList < Guid > queue )
2020-09-24 17:04:21 -04:00
{
2020-11-13 09:13:32 -05:00
// Check if queue is empty.
if ( ! queue ? . Any ( ) ? ? true )
2020-09-24 17:04:21 -04:00
{
return true ;
}
2020-10-21 09:46:50 -04:00
// Get list of users.
2020-10-22 09:51:58 -04:00
var users = Participants
. Values
2020-09-24 17:04:21 -04:00
. Select ( participant = > _userManager . GetUserById ( participant . Session . UserId ) ) ;
2020-10-21 09:46:50 -04:00
// Find problematic users.
2020-09-24 17:04:21 -04:00
var usersWithNoAccess = users . Where ( user = > ! HasAccessToQueue ( user , queue ) ) ;
2020-10-21 09:46:50 -04:00
// All users must be able to access the queue.
2020-09-24 17:04:21 -04:00
return ! usersWithNoAccess . Any ( ) ;
}
/// <inheritdoc />
public bool IsGroupEmpty ( ) = > Participants . Count = = 0 ;
/// <inheritdoc />
public void CreateGroup ( SessionInfo session , NewGroupRequest request , CancellationToken cancellationToken )
{
GroupName = request . GroupName ;
AddSession ( session ) ;
var sessionIsPlayingAnItem = session . FullNowPlayingItem ! = null ;
RestartCurrentItem ( ) ;
if ( sessionIsPlayingAnItem )
{
2020-11-15 11:03:27 -05:00
var playlist = session . NowPlayingQueue . Select ( item = > item . Id ) . ToList ( ) ;
2020-10-22 09:49:37 -04:00
PlayQueue . Reset ( ) ;
2020-09-24 17:04:21 -04:00
PlayQueue . SetPlaylist ( playlist ) ;
PlayQueue . SetPlayingItemById ( session . FullNowPlayingItem . Id ) ;
RunTimeTicks = session . FullNowPlayingItem . RunTimeTicks ? ? 0 ;
PositionTicks = session . PlayState . PositionTicks ? ? 0 ;
2020-10-21 09:46:50 -04:00
// Mantain playstate.
2020-11-13 09:13:32 -05:00
var waitingState = new WaitingGroupState ( _logger )
{
ResumePlaying = ! session . PlayState . IsPaused
} ;
2020-09-24 17:04:21 -04:00
SetState ( waitingState ) ;
}
var updateSession = NewSyncPlayGroupUpdate ( GroupUpdateType . GroupJoined , GetInfo ( ) ) ;
SendGroupUpdate ( session , SyncPlayBroadcastType . CurrentSession , updateSession , cancellationToken ) ;
2020-11-13 09:13:32 -05:00
_state . SessionJoined ( this , _state . Type , session , cancellationToken ) ;
2020-09-24 17:04:21 -04:00
2020-11-15 11:03:27 -05:00
_logger . LogInformation ( "InitGroup: {SessionId} created group {GroupId}." , session . Id , GroupId . ToString ( ) ) ;
2020-09-24 17:04:21 -04:00
}
/// <inheritdoc />
public void SessionJoin ( SessionInfo session , JoinGroupRequest request , CancellationToken cancellationToken )
{
AddSession ( session ) ;
var updateSession = NewSyncPlayGroupUpdate ( GroupUpdateType . GroupJoined , GetInfo ( ) ) ;
SendGroupUpdate ( session , SyncPlayBroadcastType . CurrentSession , updateSession , cancellationToken ) ;
var updateOthers = NewSyncPlayGroupUpdate ( GroupUpdateType . UserJoined , session . UserName ) ;
SendGroupUpdate ( session , SyncPlayBroadcastType . AllExceptCurrentSession , updateOthers , cancellationToken ) ;
2020-11-13 09:13:32 -05:00
_state . SessionJoined ( this , _state . Type , session , cancellationToken ) ;
2020-09-24 17:04:21 -04:00
2020-11-15 11:03:27 -05:00
_logger . LogInformation ( "SessionJoin: {SessionId} joined group {GroupId}." , session . Id , GroupId . ToString ( ) ) ;
2020-09-24 17:04:21 -04:00
}
/// <inheritdoc />
public void SessionRestore ( SessionInfo session , JoinGroupRequest request , CancellationToken cancellationToken )
{
var updateSession = NewSyncPlayGroupUpdate ( GroupUpdateType . GroupJoined , GetInfo ( ) ) ;
SendGroupUpdate ( session , SyncPlayBroadcastType . CurrentSession , updateSession , cancellationToken ) ;
var updateOthers = NewSyncPlayGroupUpdate ( GroupUpdateType . UserJoined , session . UserName ) ;
SendGroupUpdate ( session , SyncPlayBroadcastType . AllExceptCurrentSession , updateOthers , cancellationToken ) ;
2020-11-13 09:13:32 -05:00
_state . SessionJoined ( this , _state . Type , session , cancellationToken ) ;
2020-09-24 17:04:21 -04:00
2020-11-15 11:03:27 -05:00
_logger . LogInformation ( "SessionRestore: {SessionId} re-joined group {GroupId}." , session . Id , GroupId . ToString ( ) ) ;
2020-09-24 17:04:21 -04:00
}
/// <inheritdoc />
public void SessionLeave ( SessionInfo session , CancellationToken cancellationToken )
{
2020-11-13 09:13:32 -05:00
_state . SessionLeaving ( this , _state . Type , session , cancellationToken ) ;
2020-09-24 17:04:21 -04:00
RemoveSession ( session ) ;
var updateSession = NewSyncPlayGroupUpdate ( GroupUpdateType . GroupLeft , GroupId . ToString ( ) ) ;
SendGroupUpdate ( session , SyncPlayBroadcastType . CurrentSession , updateSession , cancellationToken ) ;
var updateOthers = NewSyncPlayGroupUpdate ( GroupUpdateType . UserLeft , session . UserName ) ;
SendGroupUpdate ( session , SyncPlayBroadcastType . AllExceptCurrentSession , updateOthers , cancellationToken ) ;
2020-11-15 11:03:27 -05:00
_logger . LogInformation ( "SessionLeave: {SessionId} left group {GroupId}." , session . Id , GroupId . ToString ( ) ) ;
2020-09-24 17:04:21 -04:00
}
/// <inheritdoc />
2020-11-13 09:13:32 -05:00
public void HandleRequest ( SessionInfo session , IGroupPlaybackRequest request , CancellationToken cancellationToken )
2020-09-24 17:04:21 -04:00
{
// 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.
2020-11-15 11:03:27 -05:00
_logger . LogInformation ( "HandleRequest: {SessionId} requested {RequestType}, group {GroupId} is {StateType}." , session . Id , request . Type , GroupId . ToString ( ) , _state . Type ) ;
2020-11-13 09:13:32 -05:00
request . Apply ( this , _state , session , cancellationToken ) ;
2020-09-24 17:04:21 -04:00
}
/// <inheritdoc />
public GroupInfoDto GetInfo ( )
{
2020-11-15 11:03:27 -05:00
var participants = Participants . Values . Select ( session = > session . Session . UserName ) . Distinct ( ) . ToList ( ) ;
return new GroupInfoDto ( GroupId , GroupName , _state . Type , participants , DateTime . UtcNow ) ;
2020-09-24 17:04:21 -04:00
}
/// <inheritdoc />
public bool HasAccessToPlayQueue ( User user )
{
2020-11-15 11:03:27 -05:00
var items = PlayQueue . GetPlaylist ( ) . Select ( item = > item . ItemId ) . ToList ( ) ;
2020-09-24 17:04:21 -04:00
return HasAccessToQueue ( user , items ) ;
}
/// <inheritdoc />
public void SetIgnoreGroupWait ( SessionInfo session , bool ignoreGroupWait )
{
2020-11-14 12:09:25 -05:00
if ( Participants . TryGetValue ( session . Id , out GroupMember value ) )
2020-09-24 17:04:21 -04:00
{
2020-11-14 12:09:25 -05:00
value . IgnoreGroupWait = ignoreGroupWait ;
2020-09-24 17:04:21 -04:00
}
}
/// <inheritdoc />
2020-11-13 09:13:32 -05:00
public void SetState ( IGroupState state )
2020-09-24 17:04:21 -04:00
{
2020-11-15 11:03:27 -05:00
_logger . LogInformation ( "SetState: {GroupId} switching from {FromStateType} to {ToStateType}." , GroupId . ToString ( ) , _state . Type , state . Type ) ;
2020-11-13 09:13:32 -05:00
this . _state = state ;
2020-09-24 17:04:21 -04:00
}
/// <inheritdoc />
public Task SendGroupUpdate < T > ( SessionInfo from , SyncPlayBroadcastType type , GroupUpdate < T > message , CancellationToken cancellationToken )
{
IEnumerable < Task > GetTasks ( )
{
foreach ( var session in FilterSessions ( from , type ) )
{
yield return _sessionManager . SendSyncPlayGroupUpdate ( session , message , cancellationToken ) ;
}
}
return Task . WhenAll ( GetTasks ( ) ) ;
}
/// <inheritdoc />
public Task SendCommand ( SessionInfo from , SyncPlayBroadcastType type , SendCommand message , CancellationToken cancellationToken )
{
IEnumerable < Task > GetTasks ( )
{
foreach ( var session in FilterSessions ( from , type ) )
{
yield return _sessionManager . SendSyncPlayCommand ( session , message , cancellationToken ) ;
}
}
return Task . WhenAll ( GetTasks ( ) ) ;
}
/// <inheritdoc />
public SendCommand NewSyncPlayCommand ( SendCommandType type )
{
2020-11-15 11:03:27 -05:00
return new SendCommand (
GroupId ,
PlayQueue . GetPlayingItemPlaylistId ( ) ,
LastActivity ,
type ,
PositionTicks ,
DateTime . UtcNow ) ;
2020-09-24 17:04:21 -04:00
}
/// <inheritdoc />
public GroupUpdate < T > NewSyncPlayGroupUpdate < T > ( GroupUpdateType type , T data )
{
2020-11-15 11:03:27 -05:00
return new GroupUpdate < T > ( GroupId , type , data ) ;
2020-09-24 17:04:21 -04:00
}
/// <inheritdoc />
public long SanitizePositionTicks ( long? positionTicks )
{
var ticks = positionTicks ? ? 0 ;
2020-11-13 09:13:32 -05:00
return Math . Clamp ( ticks , 0 , RunTimeTicks ) ;
2020-09-24 17:04:21 -04:00
}
/// <inheritdoc />
public void UpdatePing ( SessionInfo session , long ping )
{
if ( Participants . TryGetValue ( session . Id , out GroupMember value ) )
{
value . Ping = ping ;
}
}
/// <inheritdoc />
public long GetHighestPing ( )
{
long max = long . MinValue ;
foreach ( var session in Participants . Values )
{
max = Math . Max ( max , session . Ping ) ;
}
return max ;
}
/// <inheritdoc />
public void SetBuffering ( SessionInfo session , bool isBuffering )
{
if ( Participants . TryGetValue ( session . Id , out GroupMember value ) )
{
value . IsBuffering = isBuffering ;
}
}
/// <inheritdoc />
public void SetAllBuffering ( bool isBuffering )
{
foreach ( var session in Participants . Values )
{
session . IsBuffering = isBuffering ;
}
}
/// <inheritdoc />
public bool IsBuffering ( )
{
foreach ( var session in Participants . Values )
{
if ( session . IsBuffering & & ! session . IgnoreGroupWait )
{
return true ;
}
}
return false ;
}
/// <inheritdoc />
2020-11-15 11:03:27 -05:00
public bool SetPlayQueue ( IReadOnlyList < Guid > playQueue , int playingItemPosition , long startPositionTicks )
2020-09-24 17:04:21 -04:00
{
2020-10-21 09:46:50 -04:00
// Ignore on empty queue or invalid item position.
2020-11-15 11:03:27 -05:00
if ( playQueue . Count = = 0 | | playingItemPosition > = playQueue . Count | | playingItemPosition < 0 )
2020-09-24 17:04:21 -04:00
{
return false ;
}
2020-10-21 10:42:57 -04:00
// Check if participants can access the new playing queue.
2020-09-24 17:04:21 -04:00
if ( ! AllUsersHaveAccessToQueue ( playQueue ) )
{
return false ;
}
2020-10-22 09:49:37 -04:00
PlayQueue . Reset ( ) ;
2020-09-24 17:04:21 -04:00
PlayQueue . SetPlaylist ( playQueue ) ;
PlayQueue . SetPlayingItemByIndex ( playingItemPosition ) ;
var item = _libraryManager . GetItemById ( PlayQueue . GetPlayingItemId ( ) ) ;
RunTimeTicks = item . RunTimeTicks ? ? 0 ;
PositionTicks = startPositionTicks ;
LastActivity = DateTime . UtcNow ;
return true ;
}
/// <inheritdoc />
public bool SetPlayingItem ( string playlistItemId )
{
var itemFound = PlayQueue . SetPlayingItemByPlaylistId ( playlistItemId ) ;
if ( itemFound )
{
var item = _libraryManager . GetItemById ( PlayQueue . GetPlayingItemId ( ) ) ;
RunTimeTicks = item . RunTimeTicks ? ? 0 ;
}
else
{
RunTimeTicks = 0 ;
}
RestartCurrentItem ( ) ;
return itemFound ;
}
/// <inheritdoc />
2020-11-15 11:03:27 -05:00
public bool RemoveFromPlayQueue ( IReadOnlyList < string > playlistItemIds )
2020-09-24 17:04:21 -04:00
{
var playingItemRemoved = PlayQueue . RemoveFromPlaylist ( playlistItemIds ) ;
if ( playingItemRemoved )
{
var itemId = PlayQueue . GetPlayingItemId ( ) ;
if ( ! itemId . Equals ( Guid . Empty ) )
{
var item = _libraryManager . GetItemById ( itemId ) ;
RunTimeTicks = item . RunTimeTicks ? ? 0 ;
}
else
{
RunTimeTicks = 0 ;
}
RestartCurrentItem ( ) ;
}
return playingItemRemoved ;
}
/// <inheritdoc />
public bool MoveItemInPlayQueue ( string playlistItemId , int newIndex )
{
return PlayQueue . MovePlaylistItem ( playlistItemId , newIndex ) ;
}
/// <inheritdoc />
2020-11-15 11:03:27 -05:00
public bool AddToPlayQueue ( IReadOnlyList < Guid > newItems , GroupQueueMode mode )
2020-09-24 17:04:21 -04:00
{
2020-10-21 09:46:50 -04:00
// Ignore on empty list.
2020-11-15 11:03:27 -05:00
if ( newItems . Count = = 0 )
2020-09-24 17:04:21 -04:00
{
return false ;
}
2020-10-21 10:42:57 -04:00
// Check if participants can access the new playing queue.
2020-09-24 17:04:21 -04:00
if ( ! AllUsersHaveAccessToQueue ( newItems ) )
{
return false ;
}
2020-11-14 17:40:01 -05:00
if ( mode . Equals ( GroupQueueMode . QueueNext ) )
2020-09-24 17:04:21 -04:00
{
PlayQueue . QueueNext ( newItems ) ;
}
else
{
PlayQueue . Queue ( newItems ) ;
}
return true ;
}
/// <inheritdoc />
public void RestartCurrentItem ( )
{
PositionTicks = 0 ;
LastActivity = DateTime . UtcNow ;
}
/// <inheritdoc />
public bool NextItemInQueue ( )
{
var update = PlayQueue . Next ( ) ;
if ( update )
{
var item = _libraryManager . GetItemById ( PlayQueue . GetPlayingItemId ( ) ) ;
RunTimeTicks = item . RunTimeTicks ? ? 0 ;
RestartCurrentItem ( ) ;
return true ;
}
else
{
return false ;
}
}
/// <inheritdoc />
public bool PreviousItemInQueue ( )
{
var update = PlayQueue . Previous ( ) ;
if ( update )
{
var item = _libraryManager . GetItemById ( PlayQueue . GetPlayingItemId ( ) ) ;
RunTimeTicks = item . RunTimeTicks ? ? 0 ;
RestartCurrentItem ( ) ;
return true ;
}
else
{
return false ;
}
}
/// <inheritdoc />
2020-11-14 17:40:01 -05:00
public void SetRepeatMode ( GroupRepeatMode mode )
2020-11-13 09:13:32 -05:00
{
2020-11-14 17:40:01 -05:00
PlayQueue . SetRepeatMode ( mode ) ;
2020-09-24 17:04:21 -04:00
}
/// <inheritdoc />
2020-11-14 17:40:01 -05:00
public void SetShuffleMode ( GroupShuffleMode mode )
2020-11-13 09:13:32 -05:00
{
2020-11-14 17:40:01 -05:00
PlayQueue . SetShuffleMode ( mode ) ;
2020-09-24 17:04:21 -04:00
}
/// <inheritdoc />
public PlayQueueUpdate GetPlayQueueUpdate ( PlayQueueUpdateReason reason )
{
var startPositionTicks = PositionTicks ;
2020-11-13 09:13:32 -05:00
if ( _state . Type . Equals ( GroupStateType . Playing ) )
2020-09-24 17:04:21 -04:00
{
var currentTime = DateTime . UtcNow ;
var elapsedTime = currentTime - LastActivity ;
2020-10-22 09:51:58 -04:00
// Elapsed time is negative if event happens
// during the delay added to account for latency.
// In this phase clients haven't started the playback yet.
// In other words, LastActivity is in the future,
// when playback unpause is supposed to happen.
// Adjust ticks only if playback actually started.
startPositionTicks + = Math . Max ( elapsedTime . Ticks , 0 ) ;
2020-09-24 17:04:21 -04:00
}
2020-11-15 11:03:27 -05:00
return new PlayQueueUpdate (
reason ,
PlayQueue . LastChange ,
PlayQueue . GetPlaylist ( ) ,
PlayQueue . PlayingItemIndex ,
startPositionTicks ,
PlayQueue . ShuffleMode ,
PlayQueue . RepeatMode ) ;
2020-09-24 17:04:21 -04:00
}
}
}