2020-09-24 23:04:21 +02:00
using System ;
using System.Threading ;
using MediaBrowser.Controller.Session ;
2020-11-14 23:40:01 +01:00
using MediaBrowser.Controller.SyncPlay.PlaybackRequests ;
2020-09-24 23:04:21 +02:00
using MediaBrowser.Model.SyncPlay ;
using Microsoft.Extensions.Logging ;
2020-11-14 23:40:01 +01:00
namespace MediaBrowser.Controller.SyncPlay.GroupStates
2020-09-24 23:04:21 +02:00
{
/// <summary>
/// Class WaitingGroupState.
/// </summary>
/// <remarks>
/// Class is not thread-safe, external locking is required when accessing methods.
/// </remarks>
public class WaitingGroupState : AbstractGroupState
{
2020-11-16 17:40:19 +01:00
/// <summary>
/// The logger.
/// </summary>
private readonly ILogger < WaitingGroupState > _logger ;
2020-09-24 23:04:21 +02:00
/// <summary>
2020-11-13 15:13:32 +01:00
/// Initializes a new instance of the <see cref="WaitingGroupState"/> class.
2020-09-24 23:04:21 +02:00
/// </summary>
2020-11-16 17:40:19 +01:00
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
public WaitingGroupState ( ILoggerFactory loggerFactory )
: base ( loggerFactory )
2020-09-24 23:04:21 +02:00
{
2020-11-16 17:40:19 +01:00
_logger = LoggerFactory . CreateLogger < WaitingGroupState > ( ) ;
2020-09-24 23:04:21 +02:00
}
/// <inheritdoc />
2020-11-14 18:09:25 +01:00
public override GroupStateType Type { get ; } = GroupStateType . Waiting ;
2020-09-24 23:04:21 +02:00
2020-11-13 15:13:32 +01:00
/// <summary>
/// Gets or sets a value indicating whether playback should resume when group is ready.
/// </summary>
public bool ResumePlaying { get ; set ; } = false ;
/// <summary>
/// Gets or sets a value indicating whether the initial state has been set.
/// </summary>
private bool InitialStateSet { get ; set ; } = false ;
/// <summary>
/// Gets or sets the group state before the first ever event.
/// </summary>
private GroupStateType InitialState { get ; set ; }
2020-09-24 23:04:21 +02:00
/// <inheritdoc />
2020-11-13 15:13:32 +01:00
public override void SessionJoined ( IGroupStateContext context , GroupStateType prevState , SessionInfo session , CancellationToken cancellationToken )
2020-09-24 23:04:21 +02:00
{
2020-10-21 15:46:50 +02:00
// Save state if first event.
2020-09-24 23:04:21 +02:00
if ( ! InitialStateSet )
{
InitialState = prevState ;
InitialStateSet = true ;
}
2020-11-13 15:13:32 +01:00
if ( prevState . Equals ( GroupStateType . Playing ) )
{
2020-09-24 23:04:21 +02:00
ResumePlaying = true ;
2020-10-21 15:46:50 +02:00
// Pause group and compute the media playback position.
2020-09-24 23:04:21 +02:00
var currentTime = DateTime . UtcNow ;
var elapsedTime = currentTime - context . LastActivity ;
context . LastActivity = currentTime ;
2020-10-22 15:51:58 +02: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.
2020-10-21 15:46:50 +02:00
// Seek only if playback actually started.
2020-10-21 16:42:57 +02:00
context . PositionTicks + = Math . Max ( elapsedTime . Ticks , 0 ) ;
2020-09-24 23:04:21 +02:00
}
2020-10-21 15:46:50 +02:00
// Prepare new session.
2020-09-24 23:04:21 +02:00
var playQueueUpdate = context . GetPlayQueueUpdate ( PlayQueueUpdateReason . NewPlaylist ) ;
var update = context . NewSyncPlayGroupUpdate ( GroupUpdateType . PlayQueue , playQueueUpdate ) ;
context . SendGroupUpdate ( session , SyncPlayBroadcastType . CurrentSession , update , cancellationToken ) ;
context . SetBuffering ( session , true ) ;
2020-10-21 15:46:50 +02:00
// Send pause command to all non-buffering sessions.
2020-09-24 23:04:21 +02:00
var command = context . NewSyncPlayCommand ( SendCommandType . Pause ) ;
context . SendCommand ( session , SyncPlayBroadcastType . AllReady , command , cancellationToken ) ;
}
/// <inheritdoc />
2020-11-13 15:13:32 +01:00
public override void SessionLeaving ( IGroupStateContext context , GroupStateType prevState , SessionInfo session , CancellationToken cancellationToken )
2020-09-24 23:04:21 +02:00
{
2020-10-21 15:46:50 +02:00
// Save state if first event.
2020-09-24 23:04:21 +02:00
if ( ! InitialStateSet )
{
InitialState = prevState ;
InitialStateSet = true ;
}
context . SetBuffering ( session , false ) ;
if ( ! context . IsBuffering ( ) )
{
if ( ResumePlaying )
{
2020-11-16 17:40:19 +01:00
_logger . LogDebug ( "Session {SessionId} left group {GroupId}, notifying others to resume." , session . Id , context . GroupId . ToString ( ) ) ;
2020-10-21 15:46:50 +02:00
// Client, that was buffering, left the group.
2020-11-16 17:40:19 +01:00
var playingState = new PlayingGroupState ( LoggerFactory ) ;
2020-09-24 23:04:21 +02:00
context . SetState ( playingState ) ;
var unpauseRequest = new UnpauseGroupRequest ( ) ;
2020-11-13 15:13:32 +01:00
playingState . HandleRequest ( context , Type , unpauseRequest , session , cancellationToken ) ;
2020-09-24 23:04:21 +02:00
}
else
{
2020-11-16 17:40:19 +01:00
_logger . LogDebug ( "Session {SessionId} left group {GroupId}, returning to previous state." , session . Id , context . GroupId . ToString ( ) ) ;
2020-10-21 15:46:50 +02:00
// Group is ready, returning to previous state.
2020-11-16 17:40:19 +01:00
var pausedState = new PausedGroupState ( LoggerFactory ) ;
2020-09-24 23:04:21 +02:00
context . SetState ( pausedState ) ;
}
}
}
/// <inheritdoc />
2020-11-13 15:13:32 +01:00
public override void HandleRequest ( IGroupStateContext context , GroupStateType prevState , PlayGroupRequest request , SessionInfo session , CancellationToken cancellationToken )
2020-09-24 23:04:21 +02:00
{
2020-10-21 15:46:50 +02:00
// Save state if first event.
2020-09-24 23:04:21 +02:00
if ( ! InitialStateSet )
{
InitialState = prevState ;
InitialStateSet = true ;
}
ResumePlaying = true ;
var setQueueStatus = context . SetPlayQueue ( request . PlayingQueue , request . PlayingItemPosition , request . StartPositionTicks ) ;
if ( ! setQueueStatus )
{
2020-11-16 17:40:19 +01:00
_logger . LogError ( "Unable to set playing queue in group {GroupId}." , context . GroupId . ToString ( ) ) ;
2020-09-24 23:04:21 +02:00
2020-10-21 15:46:50 +02:00
// Ignore request and return to previous state.
2020-11-13 15:13:32 +01:00
IGroupState newState = prevState switch {
2020-11-16 17:40:19 +01:00
GroupStateType . Playing = > new PlayingGroupState ( LoggerFactory ) ,
GroupStateType . Paused = > new PausedGroupState ( LoggerFactory ) ,
_ = > new IdleGroupState ( LoggerFactory )
2020-11-13 15:13:32 +01:00
} ;
2020-09-24 23:04:21 +02:00
context . SetState ( newState ) ;
return ;
}
var playQueueUpdate = context . GetPlayQueueUpdate ( PlayQueueUpdateReason . NewPlaylist ) ;
var update = context . NewSyncPlayGroupUpdate ( GroupUpdateType . PlayQueue , playQueueUpdate ) ;
context . SendGroupUpdate ( session , SyncPlayBroadcastType . AllGroup , update , cancellationToken ) ;
2020-10-21 16:42:57 +02:00
// Reset status of sessions and await for all Ready events.
2020-09-24 23:04:21 +02:00
context . SetAllBuffering ( true ) ;
2020-11-16 17:40:19 +01:00
_logger . LogDebug ( "Session {SessionId} set a new play queue in group {GroupId}." , session . Id , context . GroupId . ToString ( ) ) ;
2020-09-24 23:04:21 +02:00
}
/// <inheritdoc />
2020-11-13 15:13:32 +01:00
public override void HandleRequest ( IGroupStateContext context , GroupStateType prevState , SetPlaylistItemGroupRequest request , SessionInfo session , CancellationToken cancellationToken )
2020-09-24 23:04:21 +02:00
{
2020-10-21 15:46:50 +02:00
// Save state if first event.
2020-09-24 23:04:21 +02:00
if ( ! InitialStateSet )
{
InitialState = prevState ;
InitialStateSet = true ;
}
ResumePlaying = true ;
var result = context . SetPlayingItem ( request . PlaylistItemId ) ;
if ( result )
{
var playQueueUpdate = context . GetPlayQueueUpdate ( PlayQueueUpdateReason . SetCurrentItem ) ;
var update = context . NewSyncPlayGroupUpdate ( GroupUpdateType . PlayQueue , playQueueUpdate ) ;
context . SendGroupUpdate ( session , SyncPlayBroadcastType . AllGroup , update , cancellationToken ) ;
2020-10-21 16:42:57 +02:00
// Reset status of sessions and await for all Ready events.
2020-09-24 23:04:21 +02:00
context . SetAllBuffering ( true ) ;
}
else
{
2020-10-21 15:46:50 +02:00
// Return to old state.
2020-11-13 15:13:32 +01:00
IGroupState newState = prevState switch
2020-09-24 23:04:21 +02:00
{
2020-11-16 17:40:19 +01:00
GroupStateType . Playing = > new PlayingGroupState ( LoggerFactory ) ,
GroupStateType . Paused = > new PausedGroupState ( LoggerFactory ) ,
_ = > new IdleGroupState ( LoggerFactory )
2020-11-13 15:13:32 +01:00
} ;
2020-09-24 23:04:21 +02:00
context . SetState ( newState ) ;
2020-11-16 17:40:19 +01:00
_logger . LogDebug ( "Unable to change current playing item in group {GroupId}." , context . GroupId . ToString ( ) ) ;
2020-09-24 23:04:21 +02:00
}
}
/// <inheritdoc />
2020-11-13 15:13:32 +01:00
public override void HandleRequest ( IGroupStateContext context , GroupStateType prevState , UnpauseGroupRequest request , SessionInfo session , CancellationToken cancellationToken )
2020-09-24 23:04:21 +02:00
{
2020-10-21 15:46:50 +02:00
// Save state if first event.
2020-09-24 23:04:21 +02:00
if ( ! InitialStateSet )
{
InitialState = prevState ;
InitialStateSet = true ;
}
2020-11-13 15:13:32 +01:00
if ( prevState . Equals ( GroupStateType . Idle ) )
2020-09-24 23:04:21 +02:00
{
ResumePlaying = true ;
context . RestartCurrentItem ( ) ;
var playQueueUpdate = context . GetPlayQueueUpdate ( PlayQueueUpdateReason . NewPlaylist ) ;
var update = context . NewSyncPlayGroupUpdate ( GroupUpdateType . PlayQueue , playQueueUpdate ) ;
context . SendGroupUpdate ( session , SyncPlayBroadcastType . AllGroup , update , cancellationToken ) ;
2020-10-21 16:42:57 +02:00
// Reset status of sessions and await for all Ready events.
2020-09-24 23:04:21 +02:00
context . SetAllBuffering ( true ) ;
2020-11-16 17:40:19 +01:00
_logger . LogDebug ( "Group {GroupId} is waiting for all ready events." , context . GroupId . ToString ( ) ) ;
2020-09-24 23:04:21 +02:00
}
else
{
if ( ResumePlaying )
{
2020-11-16 17:40:19 +01:00
_logger . LogDebug ( "Forcing the playback to start in group {GroupId}. Group-wait is disabled until next state change." , context . GroupId . ToString ( ) ) ;
2020-09-24 23:04:21 +02:00
2020-10-21 15:46:50 +02:00
// An Unpause request is forcing the playback to start, ignoring sessions that are not ready.
2020-09-24 23:04:21 +02:00
context . SetAllBuffering ( false ) ;
2020-10-21 15:46:50 +02:00
// Change state.
2020-11-16 17:40:19 +01:00
var playingState = new PlayingGroupState ( LoggerFactory )
2020-11-13 15:13:32 +01:00
{
IgnoreBuffering = true
} ;
2020-09-24 23:04:21 +02:00
context . SetState ( playingState ) ;
2020-11-13 15:13:32 +01:00
playingState . HandleRequest ( context , Type , request , session , cancellationToken ) ;
2020-09-24 23:04:21 +02:00
}
else
{
2020-10-21 15:46:50 +02:00
// Group would have gone to paused state, now will go to playing state when ready.
2020-09-24 23:04:21 +02:00
ResumePlaying = true ;
2020-10-21 15:46:50 +02:00
// Notify relevant state change event.
2020-09-24 23:04:21 +02:00
SendGroupStateUpdate ( context , request , session , cancellationToken ) ;
}
}
}
/// <inheritdoc />
2020-11-13 15:13:32 +01:00
public override void HandleRequest ( IGroupStateContext context , GroupStateType prevState , PauseGroupRequest request , SessionInfo session , CancellationToken cancellationToken )
2020-09-24 23:04:21 +02:00
{
2020-10-21 15:46:50 +02:00
// Save state if first event.
2020-09-24 23:04:21 +02:00
if ( ! InitialStateSet )
{
InitialState = prevState ;
InitialStateSet = true ;
}
2020-10-21 15:46:50 +02:00
// Wait for sessions to be ready, then switch to paused state.
2020-09-24 23:04:21 +02:00
ResumePlaying = false ;
2020-10-21 15:46:50 +02:00
// Notify relevant state change event.
2020-09-24 23:04:21 +02:00
SendGroupStateUpdate ( context , request , session , cancellationToken ) ;
}
/// <inheritdoc />
2020-11-13 15:13:32 +01:00
public override void HandleRequest ( IGroupStateContext context , GroupStateType prevState , StopGroupRequest request , SessionInfo session , CancellationToken cancellationToken )
2020-09-24 23:04:21 +02:00
{
2020-10-21 15:46:50 +02:00
// Save state if first event.
2020-09-24 23:04:21 +02:00
if ( ! InitialStateSet )
{
InitialState = prevState ;
InitialStateSet = true ;
}
2020-10-21 15:46:50 +02:00
// Change state.
2020-11-16 17:40:19 +01:00
var idleState = new IdleGroupState ( LoggerFactory ) ;
2020-09-24 23:04:21 +02:00
context . SetState ( idleState ) ;
2020-11-13 15:13:32 +01:00
idleState . HandleRequest ( context , Type , request , session , cancellationToken ) ;
2020-09-24 23:04:21 +02:00
}
/// <inheritdoc />
2020-11-13 15:13:32 +01:00
public override void HandleRequest ( IGroupStateContext context , GroupStateType prevState , SeekGroupRequest request , SessionInfo session , CancellationToken cancellationToken )
2020-09-24 23:04:21 +02:00
{
2020-10-21 15:46:50 +02:00
// Save state if first event.
2020-09-24 23:04:21 +02:00
if ( ! InitialStateSet )
{
InitialState = prevState ;
InitialStateSet = true ;
}
2020-11-13 15:13:32 +01:00
if ( prevState . Equals ( GroupStateType . Playing ) )
2020-09-24 23:04:21 +02:00
{
ResumePlaying = true ;
}
2020-11-13 15:13:32 +01:00
else if ( prevState . Equals ( GroupStateType . Paused ) )
2020-09-24 23:04:21 +02:00
{
ResumePlaying = false ;
}
2020-10-21 15:46:50 +02:00
// Sanitize PositionTicks.
2020-09-24 23:04:21 +02:00
var ticks = context . SanitizePositionTicks ( request . PositionTicks ) ;
2020-10-21 15:46:50 +02:00
// Seek.
2020-09-24 23:04:21 +02:00
context . PositionTicks = ticks ;
context . LastActivity = DateTime . UtcNow ;
var command = context . NewSyncPlayCommand ( SendCommandType . Seek ) ;
context . SendCommand ( session , SyncPlayBroadcastType . AllGroup , command , cancellationToken ) ;
2020-10-21 16:42:57 +02:00
// Reset status of sessions and await for all Ready events.
2020-09-24 23:04:21 +02:00
context . SetAllBuffering ( true ) ;
2020-10-21 15:46:50 +02:00
// Notify relevant state change event.
2020-09-24 23:04:21 +02:00
SendGroupStateUpdate ( context , request , session , cancellationToken ) ;
}
/// <inheritdoc />
2020-11-13 15:13:32 +01:00
public override void HandleRequest ( IGroupStateContext context , GroupStateType prevState , BufferGroupRequest request , SessionInfo session , CancellationToken cancellationToken )
2020-09-24 23:04:21 +02:00
{
2020-10-21 15:46:50 +02:00
// Save state if first event.
2020-09-24 23:04:21 +02:00
if ( ! InitialStateSet )
{
InitialState = prevState ;
InitialStateSet = true ;
}
2020-10-21 15:46:50 +02:00
// Make sure the client is playing the correct item.
2020-11-13 15:13:32 +01:00
if ( ! request . PlaylistItemId . Equals ( context . PlayQueue . GetPlayingItemPlaylistId ( ) , StringComparison . OrdinalIgnoreCase ) )
2020-09-24 23:04:21 +02:00
{
2020-11-16 17:40:19 +01:00
_logger . LogDebug ( "Session {SessionId} reported wrong playlist item in group {GroupId}." , session . Id , context . GroupId . ToString ( ) ) ;
2020-09-24 23:04:21 +02:00
var playQueueUpdate = context . GetPlayQueueUpdate ( PlayQueueUpdateReason . SetCurrentItem ) ;
var updateSession = context . NewSyncPlayGroupUpdate ( GroupUpdateType . PlayQueue , playQueueUpdate ) ;
context . SendGroupUpdate ( session , SyncPlayBroadcastType . CurrentSession , updateSession , cancellationToken ) ;
context . SetBuffering ( session , true ) ;
return ;
}
2020-11-13 15:13:32 +01:00
if ( prevState . Equals ( GroupStateType . Playing ) )
2020-09-24 23:04:21 +02:00
{
2020-10-21 15:46:50 +02:00
// Resume playback when all ready.
2020-09-24 23:04:21 +02:00
ResumePlaying = true ;
context . SetBuffering ( session , true ) ;
2020-10-21 15:46:50 +02:00
// Pause group and compute the media playback position.
2020-09-24 23:04:21 +02:00
var currentTime = DateTime . UtcNow ;
var elapsedTime = currentTime - context . LastActivity ;
context . LastActivity = currentTime ;
2020-10-22 15:51:58 +02: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.
// Seek only if playback actually started.
2020-10-21 16:42:57 +02:00
context . PositionTicks + = Math . Max ( elapsedTime . Ticks , 0 ) ;
2020-09-24 23:04:21 +02:00
2020-10-21 15:46:50 +02:00
// Send pause command to all non-buffering sessions.
2020-09-24 23:04:21 +02:00
var command = context . NewSyncPlayCommand ( SendCommandType . Pause ) ;
context . SendCommand ( session , SyncPlayBroadcastType . AllReady , command , cancellationToken ) ;
}
2020-11-13 15:13:32 +01:00
else if ( prevState . Equals ( GroupStateType . Paused ) )
2020-09-24 23:04:21 +02:00
{
2020-10-21 15:46:50 +02:00
// Don't resume playback when all ready.
2020-09-24 23:04:21 +02:00
ResumePlaying = false ;
context . SetBuffering ( session , true ) ;
2020-10-21 15:46:50 +02:00
// Send pause command to buffering session.
2020-09-24 23:04:21 +02:00
var command = context . NewSyncPlayCommand ( SendCommandType . Pause ) ;
context . SendCommand ( session , SyncPlayBroadcastType . CurrentSession , command , cancellationToken ) ;
}
2020-11-13 15:13:32 +01:00
else if ( prevState . Equals ( GroupStateType . Waiting ) )
2020-09-24 23:04:21 +02:00
{
2020-10-21 15:46:50 +02:00
// Another session is now buffering.
2020-09-24 23:04:21 +02:00
context . SetBuffering ( session , true ) ;
if ( ! ResumePlaying )
{
2020-10-21 15:46:50 +02:00
// Force update for this session that should be paused.
2020-09-24 23:04:21 +02:00
var command = context . NewSyncPlayCommand ( SendCommandType . Pause ) ;
context . SendCommand ( session , SyncPlayBroadcastType . CurrentSession , command , cancellationToken ) ;
}
}
2020-10-21 15:46:50 +02:00
// Notify relevant state change event.
2020-09-24 23:04:21 +02:00
SendGroupStateUpdate ( context , request , session , cancellationToken ) ;
}
/// <inheritdoc />
2020-11-13 15:13:32 +01:00
public override void HandleRequest ( IGroupStateContext context , GroupStateType prevState , ReadyGroupRequest request , SessionInfo session , CancellationToken cancellationToken )
2020-09-24 23:04:21 +02:00
{
2020-10-21 15:46:50 +02:00
// Save state if first event.
2020-09-24 23:04:21 +02:00
if ( ! InitialStateSet )
{
InitialState = prevState ;
InitialStateSet = true ;
}
2020-10-21 15:46:50 +02:00
// Make sure the client is playing the correct item.
2020-11-13 15:13:32 +01:00
if ( ! request . PlaylistItemId . Equals ( context . PlayQueue . GetPlayingItemPlaylistId ( ) , StringComparison . OrdinalIgnoreCase ) )
2020-09-24 23:04:21 +02:00
{
2020-11-16 17:40:19 +01:00
_logger . LogDebug ( "Session {SessionId} reported wrong playlist item in group {GroupId}." , session . Id , context . GroupId . ToString ( ) ) ;
2020-09-24 23:04:21 +02:00
var playQueueUpdate = context . GetPlayQueueUpdate ( PlayQueueUpdateReason . SetCurrentItem ) ;
var update = context . NewSyncPlayGroupUpdate ( GroupUpdateType . PlayQueue , playQueueUpdate ) ;
context . SendGroupUpdate ( session , SyncPlayBroadcastType . CurrentSession , update , cancellationToken ) ;
context . SetBuffering ( session , true ) ;
return ;
}
2020-11-08 18:08:59 +01:00
// Compute elapsed time between the client reported time and now.
// Elapsed time is used to estimate the client position when playback is unpaused.
// Ideally, the request is received and handled without major delays.
// However, to avoid waiting indefinitely when a client is not reporting a correct time,
// the elapsed time is ignored after a certain threshold.
2020-09-24 23:04:21 +02:00
var currentTime = DateTime . UtcNow ;
2020-11-08 18:08:59 +01:00
var elapsedTime = currentTime . Subtract ( request . When ) ;
var timeSyncThresholdTicks = TimeSpan . FromMilliseconds ( context . TimeSyncOffset ) . Ticks ;
if ( Math . Abs ( elapsedTime . Ticks ) > timeSyncThresholdTicks )
{
2020-11-16 17:40:19 +01:00
_logger . LogWarning ( "Session {SessionId} is not time syncing properly. Ignoring elapsed time." , session . Id ) ;
2020-11-08 18:08:59 +01:00
elapsedTime = TimeSpan . Zero ;
}
// Ignore elapsed time if client is paused.
2020-09-24 23:04:21 +02:00
if ( ! request . IsPlaying )
{
elapsedTime = TimeSpan . Zero ;
}
2020-11-08 18:08:59 +01:00
var requestTicks = context . SanitizePositionTicks ( request . PositionTicks ) ;
2020-09-24 23:04:21 +02:00
var clientPosition = TimeSpan . FromTicks ( requestTicks ) + elapsedTime ;
var delayTicks = context . PositionTicks - clientPosition . Ticks ;
2020-11-08 18:08:59 +01:00
var maxPlaybackOffsetTicks = TimeSpan . FromMilliseconds ( context . MaxPlaybackOffset ) . Ticks ;
2020-09-24 23:04:21 +02:00
2020-11-16 17:40:19 +01:00
_logger . LogDebug ( "Session {SessionId} is at {PositionTicks} (delay of {Delay} seconds) in group {GroupId}." , session . Id , clientPosition , TimeSpan . FromTicks ( delayTicks ) . TotalSeconds , context . GroupId . ToString ( ) ) ;
2020-09-24 23:04:21 +02:00
if ( ResumePlaying )
{
// Handle case where session reported as ready but in reality
2020-10-21 15:46:50 +02:00
// it has no clue of the real position nor the playback state.
2020-11-08 18:08:59 +01:00
if ( ! request . IsPlaying & & Math . Abs ( delayTicks ) > maxPlaybackOffsetTicks )
{
2020-10-21 15:46:50 +02:00
// Session not ready at all.
2020-09-24 23:04:21 +02:00
context . SetBuffering ( session , true ) ;
2020-10-21 15:46:50 +02:00
// Correcting session's position.
2020-09-24 23:04:21 +02:00
var command = context . NewSyncPlayCommand ( SendCommandType . Seek ) ;
context . SendCommand ( session , SyncPlayBroadcastType . CurrentSession , command , cancellationToken ) ;
2020-10-21 15:46:50 +02:00
// Notify relevant state change event.
2020-09-24 23:04:21 +02:00
SendGroupStateUpdate ( context , request , session , cancellationToken ) ;
2020-11-16 17:40:19 +01:00
_logger . LogWarning ( "Session {SessionId} got lost in time, correcting." , session . Id ) ;
2020-09-24 23:04:21 +02:00
return ;
}
2020-10-21 15:46:50 +02:00
// Session is ready.
2020-09-24 23:04:21 +02:00
context . SetBuffering ( session , false ) ;
if ( context . IsBuffering ( ) )
{
2020-10-21 15:46:50 +02:00
// Others are still buffering, tell this client to pause when ready.
2020-09-24 23:04:21 +02:00
var command = context . NewSyncPlayCommand ( SendCommandType . Pause ) ;
2020-11-14 23:40:01 +01:00
command . When = currentTime . AddTicks ( delayTicks ) ;
2020-09-24 23:04:21 +02:00
context . SendCommand ( session , SyncPlayBroadcastType . CurrentSession , command , cancellationToken ) ;
2020-11-16 17:40:19 +01:00
_logger . LogInformation ( "Session {SessionId} will pause when ready in {Delay} seconds. Group {GroupId} is waiting for all ready events." , session . Id , TimeSpan . FromTicks ( delayTicks ) . TotalSeconds , context . GroupId . ToString ( ) ) ;
2020-09-24 23:04:21 +02:00
}
else
{
2020-10-21 15:46:50 +02:00
// If all ready, then start playback.
// Let other clients resume as soon as the buffering client catches up.
2020-09-24 23:04:21 +02:00
if ( delayTicks > context . GetHighestPing ( ) * 2 * TimeSpan . TicksPerMillisecond )
{
2020-10-21 15:46:50 +02:00
// Client that was buffering is recovering, notifying others to resume.
2020-09-24 23:04:21 +02:00
context . LastActivity = currentTime . AddTicks ( delayTicks ) ;
var command = context . NewSyncPlayCommand ( SendCommandType . Unpause ) ;
var filter = SyncPlayBroadcastType . AllExceptCurrentSession ;
if ( ! request . IsPlaying )
{
filter = SyncPlayBroadcastType . AllGroup ;
}
context . SendCommand ( session , filter , command , cancellationToken ) ;
2020-11-16 17:40:19 +01:00
_logger . LogInformation ( "Session {SessionId} is recovering, group {GroupId} will resume in {Delay} seconds." , session . Id , context . GroupId . ToString ( ) , TimeSpan . FromTicks ( delayTicks ) . TotalSeconds ) ;
2020-09-24 23:04:21 +02:00
}
else
{
2020-10-21 15:46:50 +02:00
// Client, that was buffering, resumed playback but did not update others in time.
2020-09-24 23:04:21 +02:00
delayTicks = context . GetHighestPing ( ) * 2 * TimeSpan . TicksPerMillisecond ;
2020-10-22 15:51:58 +02:00
delayTicks = Math . Max ( delayTicks , context . DefaultPing ) ;
2020-09-24 23:04:21 +02:00
context . LastActivity = currentTime . AddTicks ( delayTicks ) ;
var command = context . NewSyncPlayCommand ( SendCommandType . Unpause ) ;
context . SendCommand ( session , SyncPlayBroadcastType . AllGroup , command , cancellationToken ) ;
2020-11-16 17:40:19 +01:00
_logger . LogWarning ( "Session {SessionId} resumed playback, group {GroupId} has {Delay} seconds to recover." , session . Id , context . GroupId . ToString ( ) , TimeSpan . FromTicks ( delayTicks ) . TotalSeconds ) ;
2020-09-24 23:04:21 +02:00
}
2020-10-21 15:46:50 +02:00
// Change state.
2020-11-16 17:40:19 +01:00
var playingState = new PlayingGroupState ( LoggerFactory ) ;
2020-09-24 23:04:21 +02:00
context . SetState ( playingState ) ;
2020-11-13 15:13:32 +01:00
playingState . HandleRequest ( context , Type , request , session , cancellationToken ) ;
2020-09-24 23:04:21 +02:00
}
}
else
{
2020-11-15 17:03:27 +01:00
// Check that session is really ready, tolerate player imperfections under a certain threshold.
2020-11-08 18:08:59 +01:00
if ( Math . Abs ( context . PositionTicks - requestTicks ) > maxPlaybackOffsetTicks )
2020-09-24 23:04:21 +02:00
{
2020-10-21 15:46:50 +02:00
// Session still not ready.
2020-09-24 23:04:21 +02:00
context . SetBuffering ( session , true ) ;
2020-10-21 15:46:50 +02:00
// Session is seeking to wrong position, correcting.
2020-09-24 23:04:21 +02:00
var command = context . NewSyncPlayCommand ( SendCommandType . Seek ) ;
context . SendCommand ( session , SyncPlayBroadcastType . CurrentSession , command , cancellationToken ) ;
2020-10-21 15:46:50 +02:00
// Notify relevant state change event.
2020-09-24 23:04:21 +02:00
SendGroupStateUpdate ( context , request , session , cancellationToken ) ;
2020-11-16 17:40:19 +01:00
_logger . LogWarning ( "Session {SessionId} is seeking to wrong position, correcting." , session . Id ) ;
2020-09-24 23:04:21 +02:00
return ;
2020-11-13 15:13:32 +01:00
}
else
{
2020-10-21 15:46:50 +02:00
// Session is ready.
2020-09-24 23:04:21 +02:00
context . SetBuffering ( session , false ) ;
}
if ( ! context . IsBuffering ( ) )
{
2020-11-16 17:40:19 +01:00
_logger . LogDebug ( "Session {SessionId} is ready, group {GroupId} is ready." , session . Id , context . GroupId . ToString ( ) ) ;
2020-10-21 15:46:50 +02:00
// Group is ready, returning to previous state.
2020-11-16 17:40:19 +01:00
var pausedState = new PausedGroupState ( LoggerFactory ) ;
2020-09-24 23:04:21 +02:00
context . SetState ( pausedState ) ;
2020-11-13 15:13:32 +01:00
if ( InitialState . Equals ( GroupStateType . Playing ) )
2020-09-24 23:04:21 +02:00
{
2020-10-21 15:46:50 +02:00
// Group went from playing to waiting state and a pause request occured while waiting.
2020-09-24 23:04:21 +02:00
var pauserequest = new PauseGroupRequest ( ) ;
2020-11-13 15:13:32 +01:00
pausedState . HandleRequest ( context , Type , pauserequest , session , cancellationToken ) ;
2020-09-24 23:04:21 +02:00
}
2020-11-13 15:13:32 +01:00
else if ( InitialState . Equals ( GroupStateType . Paused ) )
2020-09-24 23:04:21 +02:00
{
2020-11-13 15:13:32 +01:00
pausedState . HandleRequest ( context , Type , request , session , cancellationToken ) ;
2020-09-24 23:04:21 +02:00
}
}
}
}
/// <inheritdoc />
2020-12-03 15:41:34 +01:00
public override void HandleRequest ( IGroupStateContext context , GroupStateType prevState , NextItemGroupRequest request , SessionInfo session , CancellationToken cancellationToken )
2020-09-24 23:04:21 +02:00
{
2020-10-21 15:46:50 +02:00
// Save state if first event.
2020-09-24 23:04:21 +02:00
if ( ! InitialStateSet )
{
InitialState = prevState ;
InitialStateSet = true ;
}
ResumePlaying = true ;
2020-10-21 15:46:50 +02:00
// Make sure the client knows the playing item, to avoid duplicate requests.
2020-11-13 15:13:32 +01:00
if ( ! request . PlaylistItemId . Equals ( context . PlayQueue . GetPlayingItemPlaylistId ( ) , StringComparison . OrdinalIgnoreCase ) )
2020-09-24 23:04:21 +02:00
{
2020-11-16 17:40:19 +01:00
_logger . LogDebug ( "Session {SessionId} provided the wrong playlist item for group {GroupId}." , session . Id , context . GroupId . ToString ( ) ) ;
2020-09-24 23:04:21 +02:00
return ;
}
var newItem = context . NextItemInQueue ( ) ;
if ( newItem )
{
2020-10-21 15:46:50 +02:00
// Send playing-queue update.
2020-12-03 15:41:34 +01:00
var playQueueUpdate = context . GetPlayQueueUpdate ( PlayQueueUpdateReason . NextItem ) ;
2020-09-24 23:04:21 +02:00
var update = context . NewSyncPlayGroupUpdate ( GroupUpdateType . PlayQueue , playQueueUpdate ) ;
context . SendGroupUpdate ( session , SyncPlayBroadcastType . AllGroup , update , cancellationToken ) ;
2020-10-21 16:42:57 +02:00
// Reset status of sessions and await for all Ready events.
2020-09-24 23:04:21 +02:00
context . SetAllBuffering ( true ) ;
}
else
{
2020-10-21 15:46:50 +02:00
// Return to old state.
2020-11-13 15:13:32 +01:00
IGroupState newState = prevState switch
2020-09-24 23:04:21 +02:00
{
2020-11-16 17:40:19 +01:00
GroupStateType . Playing = > new PlayingGroupState ( LoggerFactory ) ,
GroupStateType . Paused = > new PausedGroupState ( LoggerFactory ) ,
_ = > new IdleGroupState ( LoggerFactory )
2020-11-13 15:13:32 +01:00
} ;
2020-09-24 23:04:21 +02:00
context . SetState ( newState ) ;
2020-12-03 15:41:34 +01:00
_logger . LogDebug ( "No next item available in group {GroupId}." , context . GroupId . ToString ( ) ) ;
2020-09-24 23:04:21 +02:00
}
}
/// <inheritdoc />
2020-12-03 15:41:34 +01:00
public override void HandleRequest ( IGroupStateContext context , GroupStateType prevState , PreviousItemGroupRequest request , SessionInfo session , CancellationToken cancellationToken )
2020-09-24 23:04:21 +02:00
{
2020-10-21 15:46:50 +02:00
// Save state if first event.
2020-09-24 23:04:21 +02:00
if ( ! InitialStateSet )
{
InitialState = prevState ;
InitialStateSet = true ;
}
ResumePlaying = true ;
2020-10-21 15:46:50 +02:00
// Make sure the client knows the playing item, to avoid duplicate requests.
2020-11-13 15:13:32 +01:00
if ( ! request . PlaylistItemId . Equals ( context . PlayQueue . GetPlayingItemPlaylistId ( ) , StringComparison . OrdinalIgnoreCase ) )
2020-09-24 23:04:21 +02:00
{
2020-11-16 17:40:19 +01:00
_logger . LogDebug ( "Session {SessionId} provided the wrong playlist item for group {GroupId}." , session . Id , context . GroupId . ToString ( ) ) ;
2020-09-24 23:04:21 +02:00
return ;
}
var newItem = context . PreviousItemInQueue ( ) ;
if ( newItem )
{
2020-10-21 15:46:50 +02:00
// Send playing-queue update.
2020-12-03 15:41:34 +01:00
var playQueueUpdate = context . GetPlayQueueUpdate ( PlayQueueUpdateReason . PreviousItem ) ;
2020-09-24 23:04:21 +02:00
var update = context . NewSyncPlayGroupUpdate ( GroupUpdateType . PlayQueue , playQueueUpdate ) ;
context . SendGroupUpdate ( session , SyncPlayBroadcastType . AllGroup , update , cancellationToken ) ;
2020-10-21 16:42:57 +02:00
// Reset status of sessions and await for all Ready events.
2020-09-24 23:04:21 +02:00
context . SetAllBuffering ( true ) ;
}
else
{
2020-10-21 15:46:50 +02:00
// Return to old state.
2020-11-13 15:13:32 +01:00
IGroupState newState = prevState switch
2020-09-24 23:04:21 +02:00
{
2020-11-16 17:40:19 +01:00
GroupStateType . Playing = > new PlayingGroupState ( LoggerFactory ) ,
GroupStateType . Paused = > new PausedGroupState ( LoggerFactory ) ,
_ = > new IdleGroupState ( LoggerFactory )
2020-11-13 15:13:32 +01:00
} ;
2020-09-24 23:04:21 +02:00
context . SetState ( newState ) ;
2020-12-03 15:41:34 +01:00
_logger . LogDebug ( "No previous item available in group {GroupId}." , context . GroupId . ToString ( ) ) ;
2020-09-24 23:04:21 +02:00
}
}
2020-11-14 18:09:25 +01:00
/// <inheritdoc />
public override void HandleRequest ( IGroupStateContext context , GroupStateType prevState , IgnoreWaitGroupRequest request , SessionInfo session , CancellationToken cancellationToken )
{
context . SetIgnoreGroupWait ( session , request . IgnoreWait ) ;
if ( ! context . IsBuffering ( ) )
{
2020-11-16 17:40:19 +01:00
_logger . LogDebug ( "Ignoring session {SessionId}, group {GroupId} is ready." , session . Id , context . GroupId . ToString ( ) ) ;
2020-11-14 18:09:25 +01:00
if ( ResumePlaying )
{
// Client, that was buffering, stopped following playback.
2020-11-16 17:40:19 +01:00
var playingState = new PlayingGroupState ( LoggerFactory ) ;
2020-11-14 18:09:25 +01:00
context . SetState ( playingState ) ;
var unpauseRequest = new UnpauseGroupRequest ( ) ;
playingState . HandleRequest ( context , Type , unpauseRequest , session , cancellationToken ) ;
}
else
{
// Group is ready, returning to previous state.
2020-11-16 17:40:19 +01:00
var pausedState = new PausedGroupState ( LoggerFactory ) ;
2020-11-14 18:09:25 +01:00
context . SetState ( pausedState ) ;
}
}
}
2020-09-24 23:04:21 +02:00
}
}