235 lines
7.1 KiB
C#
235 lines
7.1 KiB
C#
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;
|
|
}
|
|
}
|