update readme
improve scrubbing improve syncing gui
This commit is contained in:
@@ -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))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user