255 lines
8.2 KiB
C#
255 lines
8.2 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Jellyfin.Data.Enums;
|
|
using Jellyfin.Plugin.JellyLMS.Models;
|
|
using MediaBrowser.Controller.Library;
|
|
using MediaBrowser.Controller.Session;
|
|
using MediaBrowser.Model.Session;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Jellyfin.Plugin.JellyLMS.Services;
|
|
|
|
/// <summary>
|
|
/// Background service that discovers LMS players and registers them as Jellyfin sessions.
|
|
/// This enables LMS players to appear in Jellyfin's "Cast to" device picker.
|
|
/// </summary>
|
|
public class LmsDeviceDiscoveryService : IHostedService, IDisposable
|
|
{
|
|
private const string AppName = "JellyLMS";
|
|
private const string AppVersion = "1.0.0";
|
|
|
|
private readonly ILogger<LmsDeviceDiscoveryService> _logger;
|
|
private readonly IServiceProvider _serviceProvider;
|
|
private readonly ILmsApiClient _lmsClient;
|
|
private readonly LmsPlayerManager _playerManager;
|
|
private readonly ConcurrentDictionary<string, string> _registeredDeviceIds = new();
|
|
private ISessionManager? _sessionManager;
|
|
private Timer? _discoveryTimer;
|
|
private bool _disposed;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="LmsDeviceDiscoveryService"/> class.
|
|
/// </summary>
|
|
/// <param name="logger">The logger instance.</param>
|
|
/// <param name="serviceProvider">The service provider for lazy resolution.</param>
|
|
/// <param name="lmsClient">The LMS API client.</param>
|
|
/// <param name="playerManager">The LMS player manager.</param>
|
|
public LmsDeviceDiscoveryService(
|
|
ILogger<LmsDeviceDiscoveryService> logger,
|
|
IServiceProvider serviceProvider,
|
|
ILmsApiClient lmsClient,
|
|
LmsPlayerManager playerManager)
|
|
{
|
|
_logger = logger;
|
|
_serviceProvider = serviceProvider;
|
|
_lmsClient = lmsClient;
|
|
_playerManager = playerManager;
|
|
_logger.LogInformation("LMS Device Discovery Service constructed");
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
_logger.LogInformation("LMS Device Discovery Service starting");
|
|
|
|
// Run initial discovery after a delay, then every 15 seconds
|
|
_discoveryTimer = new Timer(
|
|
async _ => await DiscoverAndRegisterPlayersAsync().ConfigureAwait(false),
|
|
null,
|
|
TimeSpan.FromSeconds(10),
|
|
TimeSpan.FromSeconds(15));
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task StopAsync(CancellationToken cancellationToken)
|
|
{
|
|
_logger.LogInformation("LMS Device Discovery Service stopping");
|
|
|
|
_discoveryTimer?.Change(Timeout.Infinite, 0);
|
|
_registeredDeviceIds.Clear();
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private ISessionManager? GetSessionManager()
|
|
{
|
|
if (_sessionManager != null)
|
|
{
|
|
return _sessionManager;
|
|
}
|
|
|
|
try
|
|
{
|
|
_sessionManager = _serviceProvider.GetService<ISessionManager>();
|
|
if (_sessionManager == null)
|
|
{
|
|
_logger.LogWarning("ISessionManager not available yet");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to resolve ISessionManager");
|
|
}
|
|
|
|
return _sessionManager;
|
|
}
|
|
|
|
private async Task DiscoverAndRegisterPlayersAsync()
|
|
{
|
|
try
|
|
{
|
|
var sessionManager = GetSessionManager();
|
|
if (sessionManager == null)
|
|
{
|
|
_logger.LogDebug("Session manager not available, skipping discovery");
|
|
return;
|
|
}
|
|
|
|
// First test connection
|
|
var status = await _lmsClient.TestConnectionAsync().ConfigureAwait(false);
|
|
if (!status.IsConnected)
|
|
{
|
|
_logger.LogDebug("LMS server not connected, skipping player discovery");
|
|
return;
|
|
}
|
|
|
|
// Get all players from LMS
|
|
var players = await _playerManager.GetPlayersAsync(forceRefresh: true).ConfigureAwait(false);
|
|
|
|
_logger.LogDebug("Discovered {Count} LMS players", players.Count);
|
|
|
|
foreach (var player in players)
|
|
{
|
|
try
|
|
{
|
|
await RegisterOrRefreshPlayerSessionAsync(sessionManager, player).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to register player {Name} ({Mac})", player.Name, player.MacAddress);
|
|
}
|
|
}
|
|
|
|
// Clean up tracked device IDs for players that no longer exist
|
|
foreach (var mac in _registeredDeviceIds.Keys)
|
|
{
|
|
if (!players.Exists(p => p.MacAddress == mac))
|
|
{
|
|
_registeredDeviceIds.TryRemove(mac, out _);
|
|
_logger.LogDebug("Removed tracking for disconnected player {Mac}", mac);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error during LMS player discovery");
|
|
}
|
|
}
|
|
|
|
private async Task RegisterOrRefreshPlayerSessionAsync(ISessionManager sessionManager, LmsPlayer player)
|
|
{
|
|
// Create a unique device ID for this player
|
|
var deviceId = $"lms-{player.MacAddress}";
|
|
|
|
// Always call LogSessionActivity to keep the session alive
|
|
// This creates a new session if one doesn't exist, or refreshes the existing one
|
|
var session = await sessionManager.LogSessionActivity(
|
|
appName: AppName,
|
|
appVersion: AppVersion,
|
|
deviceId: deviceId,
|
|
deviceName: player.Name,
|
|
remoteEndPoint: player.IpAddress,
|
|
user: null).ConfigureAwait(false);
|
|
|
|
if (session == null)
|
|
{
|
|
_logger.LogWarning("Failed to create/refresh session for player {Name}", player.Name);
|
|
return;
|
|
}
|
|
|
|
// Check if this is a new registration
|
|
var isNew = !_registeredDeviceIds.ContainsKey(player.MacAddress);
|
|
|
|
// Add our controller to the session using EnsureController pattern
|
|
var libraryManager = _serviceProvider.GetRequiredService<ILibraryManager>();
|
|
var (controller, created) = session.EnsureController<LmsSessionController>(
|
|
s => new LmsSessionController(
|
|
_logger,
|
|
_lmsClient,
|
|
player,
|
|
s,
|
|
sessionManager,
|
|
libraryManager));
|
|
|
|
if (created)
|
|
{
|
|
_logger.LogInformation("Created LmsSessionController for player {Name}", player.Name);
|
|
}
|
|
|
|
// Always report capabilities to ensure they're set
|
|
// This is critical for the device to appear in "Play On" menu
|
|
var capabilities = new ClientCapabilities
|
|
{
|
|
PlayableMediaTypes = [MediaType.Audio],
|
|
SupportedCommands =
|
|
[
|
|
GeneralCommandType.VolumeUp,
|
|
GeneralCommandType.VolumeDown,
|
|
GeneralCommandType.Mute,
|
|
GeneralCommandType.Unmute,
|
|
GeneralCommandType.SetVolume,
|
|
GeneralCommandType.ToggleMute
|
|
],
|
|
SupportsMediaControl = true,
|
|
SupportsPersistentIdentifier = true
|
|
};
|
|
|
|
sessionManager.ReportCapabilities(session.Id, capabilities);
|
|
|
|
// Track this device
|
|
_registeredDeviceIds[player.MacAddress] = deviceId;
|
|
|
|
if (isNew)
|
|
{
|
|
_logger.LogInformation(
|
|
"Registered LMS player {Name} ({Mac}) as session {SessionId}",
|
|
player.Name,
|
|
player.MacAddress,
|
|
session.Id);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disposes managed resources.
|
|
/// </summary>
|
|
/// <param name="disposing">Whether to dispose managed resources.</param>
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (disposing)
|
|
{
|
|
_discoveryTimer?.Dispose();
|
|
}
|
|
|
|
_disposed = true;
|
|
}
|
|
}
|