update readme
Build Plugin / build (push) Successful in 2m5s
Release Plugin / build-and-release (push) Failing after 8s

improve scrubbing
improve syncing gui
This commit is contained in:
2025-12-14 16:16:33 +01:00
parent a8e0dcaf37
commit c967b062a2
11 changed files with 383 additions and 561 deletions
@@ -226,8 +226,10 @@ public class LmsApiClient : ILmsApiClient, IDisposable
{
try
{
_logger.LogInformation("LMS SeekAsync: player {Mac}, position {Seconds}s", playerMac, positionSeconds);
await SendCommandAsync<object>(playerMac, ["time", positionSeconds.ToString("F1", CultureInfo.InvariantCulture)])
.ConfigureAwait(false);
_logger.LogInformation("LMS SeekAsync: command sent successfully");
return true;
}
catch (Exception ex)
@@ -28,6 +28,7 @@ public class LmsSessionController : ISessionController, IDisposable
private BaseItem? _currentItem;
private Guid[] _playlist = [];
private int _playlistIndex;
private long _seekOffsetTicks; // Offset from transcoded stream start position
/// <summary>
/// Initializes a new instance of the <see cref="LmsSessionController"/> class.
@@ -88,10 +89,20 @@ public class LmsSessionController : ISessionController, IDisposable
CancellationToken cancellationToken)
{
_logger.LogInformation(
"LMS Session Controller received message {MessageType} for player {PlayerName} ({Mac})",
"LMS Session Controller received message {MessageType} for player {PlayerName} ({Mac}), data type: {DataType}",
name,
_player.Name,
_player.MacAddress);
_player.MacAddress,
data?.GetType().Name ?? "null");
// Log the data for debugging
if (data is PlaystateRequest psr)
{
_logger.LogInformation(
"PlaystateRequest: Command={Command}, SeekPositionTicks={Ticks}",
psr.Command,
psr.SeekPositionTicks);
}
try
{
@@ -170,6 +181,7 @@ public class LmsSessionController : ISessionController, IDisposable
CurrentItemId = itemId;
IsPlaying = true;
IsPaused = false;
_seekOffsetTicks = 0; // Reset seek offset when starting a new track
// Look up the item for duration info
_currentItem = _libraryManager.GetItemById(itemId);
@@ -249,17 +261,45 @@ public class LmsSessionController : ISessionController, IDisposable
return;
}
var positionTicks = (long)(status.Time * TimeSpan.TicksPerSecond);
// LMS reports time relative to the current stream, but after seeking
// we're playing a transcoded stream that starts at the seek position.
// Add the seek offset to get the actual track position.
var positionTicks = (long)(status.Time * TimeSpan.TicksPerSecond) + _seekOffsetTicks;
var isPaused = status.Mode == "pause";
// Update our local state from LMS
IsPaused = isPaused;
// Check if playback has stopped on LMS side
if (status.Mode == "stop")
// Check if playback has stopped on LMS side (track ended)
// Only advance if we're not paused - LMS can briefly report "stop" during transitions
if (status.Mode == "stop" && !IsPaused)
{
_logger.LogInformation("LMS playback stopped, reporting to Jellyfin");
await ReportPlaybackStoppedAsync().ConfigureAwait(false);
// Double-check by getting status again after a brief delay to avoid false positives
await Task.Delay(500).ConfigureAwait(false);
var confirmStatus = await _lmsClient.GetPlayerStatusAsync(_player.MacAddress).ConfigureAwait(false);
if (confirmStatus?.Mode != "stop")
{
_logger.LogDebug("LMS mode changed from stop, ignoring");
return;
}
_logger.LogInformation("LMS playback stopped, checking if we should advance to next track");
// Check if there are more tracks in the playlist
if (_playlistIndex < _playlist.Length - 1)
{
_logger.LogInformation(
"Track ended, advancing to next track (index {Index}/{Total})",
_playlistIndex + 2,
_playlist.Length);
await PlayItemAtIndexAsync(_playlistIndex + 1).ConfigureAwait(false);
}
else
{
_logger.LogInformation("Playlist finished, reporting playback stopped");
await ReportPlaybackStoppedAsync().ConfigureAwait(false);
}
return;
}
@@ -366,14 +406,34 @@ public class LmsSessionController : ISessionController, IDisposable
break;
case PlaystateCommand.Seek:
if (playstateRequest.SeekPositionTicks.HasValue)
_logger.LogInformation(
"Seek command received for player {PlayerName}, SeekPositionTicks: {Ticks}, CurrentItemId: {ItemId}, CurrentSeekOffset: {Offset}",
_player.Name,
playstateRequest.SeekPositionTicks,
CurrentItemId,
_seekOffsetTicks);
if (playstateRequest.SeekPositionTicks.HasValue && CurrentItemId.HasValue)
{
var positionSeconds = playstateRequest.SeekPositionTicks.Value / TimeSpan.TicksPerSecond;
var positionTicks = playstateRequest.SeekPositionTicks.Value;
// For HTTP streams, LMS can't seek directly - we need to restart with startTimeTicks
// Build a new URL with the seek position and restart playback
var streamUrl = BuildStreamUrlWithPosition(CurrentItemId.Value, positionTicks);
_logger.LogInformation(
"Seeking player {PlayerName} to {Seconds} seconds",
_player.Name,
positionSeconds);
await _lmsClient.SeekAsync(_player.MacAddress, positionSeconds).ConfigureAwait(false);
"Seeking by restarting stream at position {Seconds}s: {Url}",
positionTicks / TimeSpan.TicksPerSecond,
streamUrl);
await _lmsClient.PlayUrlAsync(_player.MacAddress, streamUrl).ConfigureAwait(false);
// Track the seek offset so we report the correct position
// The transcoded stream starts at 0, but we need to report the actual track position
_seekOffsetTicks = positionTicks;
_logger.LogInformation("Set seek offset to {Ticks} ticks ({Seconds}s)", positionTicks, positionTicks / TimeSpan.TicksPerSecond);
}
else
{
_logger.LogWarning("Seek command received but SeekPositionTicks or CurrentItemId is null");
}
break;
@@ -473,14 +533,29 @@ public class LmsSessionController : ISessionController, IDisposable
}
private string BuildStreamUrl(Guid itemId)
{
return BuildStreamUrlWithPosition(itemId, 0);
}
private string BuildStreamUrlWithPosition(Guid itemId, long startPositionTicks)
{
var config = Plugin.Instance?.Configuration;
var jellyfinUrl = config?.JellyfinServerUrl?.TrimEnd('/') ?? "http://localhost:8096";
var apiKey = config?.JellyfinApiKey ?? string.Empty;
// Build audio stream URL that LMS can fetch
// Use direct stream endpoint - simpler and more compatible
var url = $"{jellyfinUrl}/Audio/{itemId}/stream?static=true";
string url;
if (startPositionTicks > 0)
{
// For seeking, we need to use transcoding (static=true doesn't support startTimeTicks)
// Use MP3 transcoding with the start position
url = $"{jellyfinUrl}/Audio/{itemId}/stream.mp3?audioCodec=mp3&audioBitRate=320000&startTimeTicks={startPositionTicks}";
}
else
{
// For normal playback from start, use static streaming (better quality, no transcoding)
url = $"{jellyfinUrl}/Audio/{itemId}/stream.mp3?static=true";
}
if (!string.IsNullOrEmpty(apiKey))
{