First POC with working playback
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Plugin.JellyLMS.Models;
|
||||
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 (controller, created) = session.EnsureController<LmsSessionController>(
|
||||
s => new LmsSessionController(
|
||||
_logger,
|
||||
_lmsClient,
|
||||
player,
|
||||
s));
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user