using System; using System.Collections.Generic; using Jellyfin.Plugin.JellyLMS.Models; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.JellyLMS.Services; /// /// Event args for state transitions. /// public class StateTransitionEventArgs : EventArgs { /// /// Gets the state before the transition. /// public PlaybackState FromState { get; init; } /// /// Gets the state after the transition. /// public PlaybackState ToState { get; init; } /// /// Gets the reason for the transition. /// public string? Reason { get; init; } } /// /// Manages playback state transitions with validation. /// public class PlaybackStateMachine { private readonly object _lock = new(); private readonly ILogger? _logger; private PlaybackState _currentState = PlaybackState.Idle; private PlaybackState _stateBeforeSeek = PlaybackState.Idle; /// /// Valid state transitions. Key is the current state, value is the array of valid next states. /// private static readonly Dictionary 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] }; /// /// Initializes a new instance of the class. /// /// Optional logger for state transitions. public PlaybackStateMachine(ILogger? logger = null) { _logger = logger; } /// /// Fired when the state changes. /// public event EventHandler? StateChanged; /// /// Gets the current playback state. /// public PlaybackState CurrentState { get { lock (_lock) { return _currentState; } } } /// /// Gets the state before the current seek operation (if in Seeking state). /// Used to restore the correct state after seeking completes. /// public PlaybackState StateBeforeSeek { get { lock (_lock) { return _stateBeforeSeek; } } } /// /// Gets a value indicating whether playback is active (Playing or Paused). /// public bool IsPlaybackActive { get { lock (_lock) { return _currentState == PlaybackState.Playing || _currentState == PlaybackState.Paused || _currentState == PlaybackState.Seeking || _currentState == PlaybackState.Loading; } } } /// /// Attempts to transition to a new state. /// /// The target state. /// Optional reason for the transition (for logging). /// True if the transition was valid and completed. 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; } } /// /// Forces a state change without validation. Use with caution. /// Intended for error recovery scenarios. /// /// The target state. /// Reason for the forced transition. 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"); } } } /// /// Resets the state machine to Idle. /// public void Reset() { ForceState(PlaybackState.Idle, "Reset"); } /// /// Checks if a transition from one state to another is valid. /// /// The current state. /// The target state. /// True if the transition is valid. public static bool IsValidTransition(PlaybackState from, PlaybackState to) { if (!ValidTransitions.TryGetValue(from, out var validTargets)) { return false; } return Array.IndexOf(validTargets, to) >= 0; } }