remove redundant restAPI
playback is controlled by state machine
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Plugin.JellyLMS.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.JellyLMS.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Polls LMS player status to confirm state transitions.
|
||||
/// </summary>
|
||||
public class LmsStatusPoller
|
||||
{
|
||||
private readonly ILmsApiClient _lmsClient;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LmsStatusPoller"/> class.
|
||||
/// </summary>
|
||||
/// <param name="lmsClient">The LMS API client.</param>
|
||||
/// <param name="logger">The logger instance.</param>
|
||||
public LmsStatusPoller(ILmsApiClient lmsClient, ILogger logger)
|
||||
{
|
||||
_lmsClient = lmsClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private static PluginConfiguration Config => Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
||||
|
||||
/// <summary>
|
||||
/// Waits for LMS to report that playback has started (mode="play").
|
||||
/// </summary>
|
||||
/// <param name="playerMac">The player's MAC address.</param>
|
||||
/// <param name="timeout">Maximum time to wait.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>True if playback started within the timeout.</returns>
|
||||
public async Task<bool> WaitForPlaybackStartAsync(
|
||||
string playerMac,
|
||||
TimeSpan timeout,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await WaitForModeAsync(playerMac, "play", timeout, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for LMS to report a specific playback mode.
|
||||
/// </summary>
|
||||
/// <param name="playerMac">The player's MAC address.</param>
|
||||
/// <param name="expectedMode">The expected mode ("play", "pause", "stop").</param>
|
||||
/// <param name="timeout">Maximum time to wait.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>True if the expected mode was detected within the timeout.</returns>
|
||||
public async Task<bool> WaitForModeAsync(
|
||||
string playerMac,
|
||||
string expectedMode,
|
||||
TimeSpan timeout,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var pollInterval = TimeSpan.FromMilliseconds(Config.TransitionPollIntervalMs);
|
||||
var startTime = DateTime.UtcNow;
|
||||
|
||||
_logger.LogDebug(
|
||||
"Waiting for player {Mac} to reach mode '{Mode}' (timeout: {Timeout}s)",
|
||||
playerMac,
|
||||
expectedMode,
|
||||
timeout.TotalSeconds);
|
||||
|
||||
while (DateTime.UtcNow - startTime < timeout)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
var status = await _lmsClient.GetPlayerStatusAsync(playerMac).ConfigureAwait(false);
|
||||
if (status?.Mode == expectedMode)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Player {Mac} reached mode '{Mode}' after {Elapsed}ms",
|
||||
playerMac,
|
||||
expectedMode,
|
||||
(DateTime.UtcNow - startTime).TotalMilliseconds);
|
||||
return true;
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Player {Mac} current mode: '{CurrentMode}', waiting for '{ExpectedMode}'",
|
||||
playerMac,
|
||||
status?.Mode ?? "unknown",
|
||||
expectedMode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error polling player {Mac} status", playerMac);
|
||||
}
|
||||
|
||||
await Task.Delay(pollInterval, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_logger.LogWarning(
|
||||
"Timeout waiting for player {Mac} to reach mode '{Mode}' after {Timeout}s",
|
||||
playerMac,
|
||||
expectedMode,
|
||||
timeout.TotalSeconds);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for LMS to report a position within tolerance of the target.
|
||||
/// Used to confirm seek operations completed.
|
||||
/// </summary>
|
||||
/// <param name="playerMac">The player's MAC address.</param>
|
||||
/// <param name="targetPositionSeconds">The target position in seconds.</param>
|
||||
/// <param name="toleranceSeconds">Acceptable tolerance (default 2 seconds).</param>
|
||||
/// <param name="timeout">Maximum time to wait.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>True if the position was reached within the timeout.</returns>
|
||||
public async Task<bool> WaitForSeekCompleteAsync(
|
||||
string playerMac,
|
||||
double targetPositionSeconds,
|
||||
double toleranceSeconds,
|
||||
TimeSpan timeout,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var pollInterval = TimeSpan.FromMilliseconds(Config.TransitionPollIntervalMs);
|
||||
var startTime = DateTime.UtcNow;
|
||||
|
||||
_logger.LogDebug(
|
||||
"Waiting for player {Mac} to seek to {Target}s (tolerance: {Tolerance}s, timeout: {Timeout}s)",
|
||||
playerMac,
|
||||
targetPositionSeconds,
|
||||
toleranceSeconds,
|
||||
timeout.TotalSeconds);
|
||||
|
||||
while (DateTime.UtcNow - startTime < timeout)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
var status = await _lmsClient.GetPlayerStatusAsync(playerMac).ConfigureAwait(false);
|
||||
if (status != null)
|
||||
{
|
||||
var positionDiff = Math.Abs(status.Time - targetPositionSeconds);
|
||||
if (positionDiff <= toleranceSeconds)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Player {Mac} reached position {Position}s (target: {Target}s) after {Elapsed}ms",
|
||||
playerMac,
|
||||
status.Time,
|
||||
targetPositionSeconds,
|
||||
(DateTime.UtcNow - startTime).TotalMilliseconds);
|
||||
return true;
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Player {Mac} at position {Position}s, waiting for {Target}s (diff: {Diff}s)",
|
||||
playerMac,
|
||||
status.Time,
|
||||
targetPositionSeconds,
|
||||
positionDiff);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error polling player {Mac} status during seek", playerMac);
|
||||
}
|
||||
|
||||
await Task.Delay(pollInterval, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_logger.LogWarning(
|
||||
"Timeout waiting for player {Mac} to seek to {Target}s after {Timeout}s",
|
||||
playerMac,
|
||||
targetPositionSeconds,
|
||||
timeout.TotalSeconds);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes an action with automatic retry on failure.
|
||||
/// </summary>
|
||||
/// <param name="action">The async action to execute.</param>
|
||||
/// <param name="maxRetries">Maximum number of retries.</param>
|
||||
/// <param name="onRetry">Optional callback when retrying.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>True if the action succeeded within retry limit.</returns>
|
||||
public async Task<bool> ExecuteWithRetryAsync(
|
||||
Func<Task<bool>> action,
|
||||
int maxRetries,
|
||||
Action<int>? onRetry = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var retryCount = 0;
|
||||
var baseDelayMs = 500;
|
||||
|
||||
while (retryCount <= maxRetries)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
if (await action().ConfigureAwait(false))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (retryCount < maxRetries)
|
||||
{
|
||||
_logger.LogWarning(ex, "Action failed, will retry ({Retry}/{Max})", retryCount + 1, maxRetries);
|
||||
}
|
||||
|
||||
retryCount++;
|
||||
if (retryCount <= maxRetries)
|
||||
{
|
||||
onRetry?.Invoke(retryCount);
|
||||
|
||||
// Exponential backoff: 500ms, 1000ms, 2000ms, etc.
|
||||
var delayMs = baseDelayMs * (int)Math.Pow(2, retryCount - 1);
|
||||
_logger.LogDebug("Retrying in {Delay}ms (attempt {Retry}/{Max})", delayMs, retryCount, maxRetries);
|
||||
await Task.Delay(delayMs, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user