aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorfmarcac <188743521+fmarcac@users.noreply.github.com>2026-09-15 11:13:44 -0400
committerCody Robibero <cody@robibe.ro>2026-09-15 11:13:44 -0400
commitb70e7f60ffe19847ef6259a13075c26fb6994363 (patch)
tree224385abb6d0cf0399f0d7583e5112b2c6c0566e /tests
parentc8838e704f258f69bbe7b24723c493b0966477cd (diff)
Backport pull request #17797 from jellyfin/release-12.z
Correct SyncPlay sessions that report playback at a stale position Original-merge: a93180d35f3528a556b1c81aac4ba4132b34391e Merged-by: crobibero <cody@robibe.ro> Backported-by: Cody Robibero <cody@robibe.ro>
Diffstat (limited to 'tests')
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/SyncPlay/WaitingGroupStateTests.cs126
1 files changed, 126 insertions, 0 deletions
diff --git a/tests/Jellyfin.Server.Implementations.Tests/SyncPlay/WaitingGroupStateTests.cs b/tests/Jellyfin.Server.Implementations.Tests/SyncPlay/WaitingGroupStateTests.cs
index 0cccd5d4ca..81af12ba8c 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/SyncPlay/WaitingGroupStateTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/SyncPlay/WaitingGroupStateTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations.Entities;
@@ -20,6 +21,128 @@ namespace Jellyfin.Server.Implementations.Tests.SyncPlay;
public class WaitingGroupStateTests
{
[Fact]
+ public void Ready_PlayingSessionReportsPositionFromBeforeSeek_IsCorrected()
+ {
+ var harness = new GroupHarness();
+ var group = harness.Group;
+
+ group.PositionTicks = TimeSpan.FromMinutes(10).Ticks;
+ group.LastActivity = DateTime.UtcNow;
+
+ var state = new WaitingGroupState(NullLoggerFactory.Instance) { ResumePlaying = true };
+
+ // One member seeks half an hour in.
+ state.HandleRequest(
+ new SeekGroupRequest(TimeSpan.FromMinutes(40).Ticks),
+ group,
+ GroupStateType.Playing,
+ harness.Second,
+ CancellationToken.None);
+
+ harness.Commands.Clear();
+
+ // The other member has not applied the seek yet and reports the old position, still playing.
+ state.HandleRequest(
+ new ReadyGroupRequest(DateTime.UtcNow, TimeSpan.FromMinutes(10).Ticks, true, harness.PlaylistItemId),
+ group,
+ GroupStateType.Waiting,
+ harness.First,
+ CancellationToken.None);
+
+ // It must be seeked into position, not accepted as ready and handed a pause command
+ // scheduled the length of the seek into the future.
+ Assert.Contains(harness.Commands, c => c.Command == SendCommandType.Seek);
+ Assert.DoesNotContain(harness.Commands, c => c.Command == SendCommandType.Pause);
+ Assert.True(group.IsBuffering(), "session should still be considered buffering");
+ }
+
+ [Fact]
+ public void Ready_PlayingSessionRecoveringFromALongStall_IsNotSeeked()
+ {
+ var harness = new GroupHarness();
+ var group = harness.Group;
+
+ group.PositionTicks = TimeSpan.FromMinutes(10).Ticks;
+ group.LastActivity = DateTime.UtcNow;
+
+ var state = new WaitingGroupState(NullLoggerFactory.Instance) { ResumePlaying = true };
+
+ // The session reports it is buffering. No seek happens, so the group position stays put.
+ state.HandleRequest(
+ new BufferGroupRequest(DateTime.UtcNow, group.PositionTicks, true, harness.PlaylistItemId),
+ group,
+ GroupStateType.Playing,
+ harness.First,
+ CancellationToken.None);
+
+ harness.Commands.Clear();
+
+ // It recovers 45 seconds later, still behind, and must be waited for rather than seeked
+ // forward past content it already buffered.
+ var behind = group.PositionTicks - TimeSpan.FromSeconds(45).Ticks;
+ state.HandleRequest(
+ new ReadyGroupRequest(DateTime.UtcNow, behind, true, harness.PlaylistItemId),
+ group,
+ GroupStateType.Waiting,
+ harness.First,
+ CancellationToken.None);
+
+ Assert.DoesNotContain(harness.Commands, c => c.Command == SendCommandType.Seek);
+ }
+
+ [Fact]
+ public void Ready_PlayingSessionSlightlyBehindGroup_IsStillTreatedAsCatchingUp()
+ {
+ var harness = new GroupHarness();
+ var group = harness.Group;
+
+ // A session that is a couple of seconds behind is genuinely recovering, and the group
+ // is expected to wait for it rather than seek it around.
+ group.PositionTicks = TimeSpan.FromMinutes(30).Ticks;
+ group.LastActivity = DateTime.UtcNow;
+ group.SetBuffering(harness.First, true);
+ group.SetBuffering(harness.Second, true);
+
+ var state = new WaitingGroupState(NullLoggerFactory.Instance) { ResumePlaying = true };
+ harness.Commands.Clear();
+
+ var clientPosition = group.PositionTicks - TimeSpan.FromSeconds(2).Ticks;
+ state.HandleRequest(
+ new ReadyGroupRequest(DateTime.UtcNow, clientPosition, true, harness.PlaylistItemId),
+ group,
+ GroupStateType.Waiting,
+ harness.First,
+ CancellationToken.None);
+
+ Assert.DoesNotContain(harness.Commands, c => c.Command == SendCommandType.Seek);
+ Assert.Contains(harness.Commands, c => c.Command == SendCommandType.Pause);
+ }
+
+ [Fact]
+ public void Ready_PausedSessionOutOfPosition_IsStillCorrected()
+ {
+ var harness = new GroupHarness();
+ var group = harness.Group;
+
+ group.PositionTicks = TimeSpan.FromMinutes(30).Ticks;
+ group.LastActivity = DateTime.UtcNow;
+ group.SetBuffering(harness.First, true);
+ group.SetBuffering(harness.Second, true);
+
+ var state = new WaitingGroupState(NullLoggerFactory.Instance) { ResumePlaying = true };
+ harness.Commands.Clear();
+
+ state.HandleRequest(
+ new ReadyGroupRequest(DateTime.UtcNow, 0, false, harness.PlaylistItemId),
+ group,
+ GroupStateType.Waiting,
+ harness.First,
+ CancellationToken.None);
+
+ Assert.Contains(harness.Commands, c => c.Command == SendCommandType.Seek);
+ }
+
+ [Fact]
public void Ready_ClientResumedWithLowPing_AppliesTheDefaultPingFloorInMilliseconds()
{
var harness = new GroupHarness();
@@ -99,6 +222,7 @@ public class WaitingGroupStateTests
sessionManager
.Setup(m => m.SendSyncPlayCommand(It.IsAny<string>(), It.IsAny<SendCommand>(), It.IsAny<CancellationToken>()))
+ .Callback<string, SendCommand, CancellationToken>((_, command, _) => Commands.Add(command))
.Returns(Task.CompletedTask);
sessionManager
@@ -137,5 +261,7 @@ public class WaitingGroupStateTests
public SessionInfo Second { get; }
public Guid PlaylistItemId { get; }
+
+ public List<SendCommand> Commands { get; } = new List<SendCommand>();
}
}