Use client-reported position for idle playback cleanup

This commit is contained in:
itsb
2026-08-13 16:41:45 -05:00
parent 916c3c9cc3
commit 499e64b1c3
4 changed files with 191 additions and 1 deletions
@@ -655,7 +655,7 @@ namespace Emby.Server.Implementations.Session
ItemId = session.NowPlayingItem is null ? Guid.Empty : session.NowPlayingItem.Id,
SessionId = session.Id,
MediaSourceId = session.PlayState?.MediaSourceId,
PositionTicks = session.PlayState?.PositionTicks
PositionTicks = session.LastPlaybackCheckInPositionTicks ?? 0
}).ConfigureAwait(false);
}
catch (Exception ex)
@@ -28,6 +28,7 @@ namespace MediaBrowser.Controller.Session
private readonly Lock _progressLock = new();
private Timer _progressTimer;
private PlaybackProgressInfo _lastProgressInfo;
private long? _lastPlaybackCheckInPositionTicks;
private bool _disposed;
@@ -124,6 +125,22 @@ namespace MediaBrowser.Controller.Session
/// <value>The last playback check in.</value>
public DateTime LastPlaybackCheckIn { get; set; }
/// <summary>
/// Gets the position reported by the client at the last playback check-in.
/// </summary>
/// <value>The position ticks, or <see langword="null"/> if the client did not report a position.</value>
[JsonIgnore]
public long? LastPlaybackCheckInPositionTicks
{
get
{
lock (_progressLock)
{
return _lastPlaybackCheckInPositionTicks;
}
}
}
/// <summary>
/// Gets or sets the last paused date.
/// </summary>
@@ -372,6 +389,7 @@ namespace MediaBrowser.Controller.Session
lock (_progressLock)
{
_lastPlaybackCheckInPositionTicks = progressInfo.PositionTicks;
_lastProgressInfo = progressInfo;
if (_progressTimer is null)
@@ -0,0 +1,80 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.SessionManager;
public class IdlePlaybackTests
{
[Theory]
[InlineData(null, 0)]
[InlineData(123456789L, 123456789L)]
public async Task CheckForIdlePlayback_StopsAtLastClientReportedPosition(long? clientPositionTicks, long expectedPositionTicks)
{
var playbackStopped = new TaskCompletionSource<long?>(TaskCreationOptions.RunContinuationsAsynchronously);
var eventManager = new Mock<IEventManager>();
eventManager
.Setup(manager => manager.PublishAsync(It.IsAny<PlaybackStopEventArgs>()))
.Callback<PlaybackStopEventArgs>(eventArgs => playbackStopped.TrySetResult(eventArgs.PlaybackPositionTicks))
.Returns(Task.CompletedTask);
await using var sessionManager = new Emby.Server.Implementations.Session.SessionManager(
NullLogger<Emby.Server.Implementations.Session.SessionManager>.Instance,
eventManager.Object,
Mock.Of<IUserDataManager>(),
Mock.Of<IServerConfigurationManager>(),
Mock.Of<ILibraryManager>(),
Mock.Of<IUserManager>(),
Mock.Of<IMusicManager>(),
Mock.Of<IDtoService>(),
Mock.Of<IImageProcessor>(),
Mock.Of<IServerApplicationHost>(),
Mock.Of<IDeviceManager>(),
Mock.Of<IMediaSourceManager>(),
Mock.Of<IHostApplicationLifetime>());
var session = await sessionManager.LogSessionActivity(
"Test Client",
"1.0.0",
"test-device",
"Test Device",
"127.0.0.1",
null);
session.NowPlayingItem = new BaseItemDto
{
Id = Guid.NewGuid(),
Name = "Test Item"
};
session.PlayState.PositionTicks = 987654321;
if (clientPositionTicks.HasValue)
{
session.StartAutomaticProgress(new PlaybackProgressInfo
{
IsPaused = true,
PositionTicks = clientPositionTicks
});
session.StopAutomaticProgress();
}
var idlePlaybackCallback = typeof(Emby.Server.Implementations.Session.SessionManager)
.GetMethod("CheckForIdlePlayback", BindingFlags.Instance | BindingFlags.NonPublic)!;
idlePlaybackCallback.Invoke(sessionManager, new object?[] { null });
var stoppedPositionTicks = await playbackStopped.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
Assert.Equal(expectedPositionTicks, stoppedPositionTicks);
}
}
@@ -0,0 +1,92 @@
using System;
using System.Threading.Tasks;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.SessionManager;
public class SessionInfoTests
{
[Fact]
public async Task StartAutomaticProgress_SnapshotsClientReportedPosition()
{
await using var session = new SessionInfo(Mock.Of<ISessionManager>(), NullLogger.Instance);
var progressInfo = new PlaybackProgressInfo
{
IsPaused = true,
PositionTicks = 123456789
};
session.StartAutomaticProgress(progressInfo);
Assert.Equal(progressInfo.PositionTicks, session.LastPlaybackCheckInPositionTicks);
}
[Fact]
public async Task AutomaticProgress_AdvancesEstimatedPositionWithoutAdvancingSnapshot()
{
var sessionManager = new Mock<ISessionManager>();
await using var session = new SessionInfo(sessionManager.Object, NullLogger.Instance);
var automaticProgress = new TaskCompletionSource<long?>(TaskCreationOptions.RunContinuationsAsynchronously);
const long reportedPositionTicks = 123456789;
sessionManager
.Setup(manager => manager.OnPlaybackProgress(It.IsAny<PlaybackProgressInfo>(), true))
.Callback<PlaybackProgressInfo, bool>((info, _) =>
{
session.PlayState.PositionTicks = info.PositionTicks;
automaticProgress.TrySetResult(info.PositionTicks);
})
.Returns(Task.CompletedTask);
session.PlayState.PositionTicks = reportedPositionTicks;
session.StartAutomaticProgress(new PlaybackProgressInfo
{
PositionTicks = reportedPositionTicks
});
var estimatedPositionTicks = await automaticProgress.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
session.StopAutomaticProgress();
Assert.Equal(reportedPositionTicks + TimeSpan.TicksPerSecond, estimatedPositionTicks);
Assert.Equal(estimatedPositionTicks, session.PlayState.PositionTicks);
Assert.Equal(reportedPositionTicks, session.LastPlaybackCheckInPositionTicks);
}
[Fact]
public async Task StartAutomaticProgress_ReplacesSnapshotOnLaterClientReport()
{
await using var session = new SessionInfo(Mock.Of<ISessionManager>(), NullLogger.Instance);
session.StartAutomaticProgress(new PlaybackProgressInfo
{
IsPaused = true,
PositionTicks = 123456789
});
session.StartAutomaticProgress(new PlaybackProgressInfo
{
IsPaused = true,
PositionTicks = 987654321
});
Assert.Equal(987654321, session.LastPlaybackCheckInPositionTicks);
}
[Fact]
public async Task StartAutomaticProgress_PreservesExactPausedPosition()
{
await using var session = new SessionInfo(Mock.Of<ISessionManager>(), NullLogger.Instance);
var pausedProgress = new PlaybackProgressInfo
{
IsPaused = true,
PositionTicks = 314159265
};
session.StartAutomaticProgress(pausedProgress);
Assert.Equal(pausedProgress.PositionTicks, session.LastPlaybackCheckInPositionTicks);
}
}