remove redundant restAPI
playback is controlled by state machine
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Plugin.JellyLMS.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.JellyLMS.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Event args for state transitions.
|
||||
/// </summary>
|
||||
public class StateTransitionEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the state before the transition.
|
||||
/// </summary>
|
||||
public PlaybackState FromState { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state after the transition.
|
||||
/// </summary>
|
||||
public PlaybackState ToState { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reason for the transition.
|
||||
/// </summary>
|
||||
public string? Reason { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manages playback state transitions with validation.
|
||||
/// </summary>
|
||||
public class PlaybackStateMachine
|
||||
{
|
||||
private readonly object _lock = new();
|
||||
private readonly ILogger? _logger;
|
||||
private PlaybackState _currentState = PlaybackState.Idle;
|
||||
private PlaybackState _stateBeforeSeek = PlaybackState.Idle;
|
||||
|
||||
/// <summary>
|
||||
/// Valid state transitions. Key is the current state, value is the array of valid next states.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<PlaybackState, PlaybackState[]> ValidTransitions = new()
|
||||
{
|
||||
[PlaybackState.Idle] = [PlaybackState.Loading, PlaybackState.Stopped],
|
||||
[PlaybackState.Loading] = [PlaybackState.Playing, PlaybackState.Error, PlaybackState.Stopped],
|
||||
[PlaybackState.Playing] = [PlaybackState.Paused, PlaybackState.Seeking, PlaybackState.Loading, PlaybackState.Stopped, PlaybackState.Error],
|
||||
[PlaybackState.Paused] = [PlaybackState.Playing, PlaybackState.Seeking, PlaybackState.Loading, PlaybackState.Stopped, PlaybackState.Error],
|
||||
[PlaybackState.Seeking] = [PlaybackState.Playing, PlaybackState.Paused, PlaybackState.Error, PlaybackState.Stopped],
|
||||
[PlaybackState.Error] = [PlaybackState.Loading, PlaybackState.Idle, PlaybackState.Stopped],
|
||||
[PlaybackState.Stopped] = [PlaybackState.Idle, PlaybackState.Loading]
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PlaybackStateMachine"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Optional logger for state transitions.</param>
|
||||
public PlaybackStateMachine(ILogger? logger = null)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the state changes.
|
||||
/// </summary>
|
||||
public event EventHandler<StateTransitionEventArgs>? StateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current playback state.
|
||||
/// </summary>
|
||||
public PlaybackState CurrentState
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _currentState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state before the current seek operation (if in Seeking state).
|
||||
/// Used to restore the correct state after seeking completes.
|
||||
/// </summary>
|
||||
public PlaybackState StateBeforeSeek
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _stateBeforeSeek;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether playback is active (Playing or Paused).
|
||||
/// </summary>
|
||||
public bool IsPlaybackActive
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _currentState == PlaybackState.Playing
|
||||
|| _currentState == PlaybackState.Paused
|
||||
|| _currentState == PlaybackState.Seeking
|
||||
|| _currentState == PlaybackState.Loading;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to transition to a new state.
|
||||
/// </summary>
|
||||
/// <param name="newState">The target state.</param>
|
||||
/// <param name="reason">Optional reason for the transition (for logging).</param>
|
||||
/// <returns>True if the transition was valid and completed.</returns>
|
||||
public bool TryTransition(PlaybackState newState, string? reason = null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_currentState == newState)
|
||||
{
|
||||
return true; // Already in this state
|
||||
}
|
||||
|
||||
if (!IsValidTransition(_currentState, newState))
|
||||
{
|
||||
_logger?.LogWarning(
|
||||
"Invalid state transition attempted: {From} -> {To} (reason: {Reason})",
|
||||
_currentState,
|
||||
newState,
|
||||
reason ?? "none");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store state before seek for restoration
|
||||
if (newState == PlaybackState.Seeking)
|
||||
{
|
||||
_stateBeforeSeek = _currentState;
|
||||
}
|
||||
|
||||
var oldState = _currentState;
|
||||
_currentState = newState;
|
||||
|
||||
_logger?.LogInformation(
|
||||
"State transition: {From} -> {To} (reason: {Reason})",
|
||||
oldState,
|
||||
newState,
|
||||
reason ?? "none");
|
||||
|
||||
// Fire event outside the lock to prevent deadlocks
|
||||
var args = new StateTransitionEventArgs
|
||||
{
|
||||
FromState = oldState,
|
||||
ToState = newState,
|
||||
Reason = reason
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
StateChanged?.Invoke(this, args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error in StateChanged event handler");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces a state change without validation. Use with caution.
|
||||
/// Intended for error recovery scenarios.
|
||||
/// </summary>
|
||||
/// <param name="newState">The target state.</param>
|
||||
/// <param name="reason">Reason for the forced transition.</param>
|
||||
public void ForceState(PlaybackState newState, string reason)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var oldState = _currentState;
|
||||
_currentState = newState;
|
||||
|
||||
_logger?.LogWarning(
|
||||
"Forced state transition: {From} -> {To} (reason: {Reason})",
|
||||
oldState,
|
||||
newState,
|
||||
reason);
|
||||
|
||||
var args = new StateTransitionEventArgs
|
||||
{
|
||||
FromState = oldState,
|
||||
ToState = newState,
|
||||
Reason = $"FORCED: {reason}"
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
StateChanged?.Invoke(this, args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error in StateChanged event handler");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the state machine to Idle.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
ForceState(PlaybackState.Idle, "Reset");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a transition from one state to another is valid.
|
||||
/// </summary>
|
||||
/// <param name="from">The current state.</param>
|
||||
/// <param name="to">The target state.</param>
|
||||
/// <returns>True if the transition is valid.</returns>
|
||||
public static bool IsValidTransition(PlaybackState from, PlaybackState to)
|
||||
{
|
||||
if (!ValidTransitions.TryGetValue(from, out var validTargets))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Array.IndexOf(validTargets, to) >= 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user