Apply the SyncPlay resume delay floor in the correct unit

This commit is contained in:
fmarcac
2026-09-05 14:20:26 +02:00
parent c80f05fad1
commit 0c05d9d1a9
2 changed files with 110 additions and 1 deletions
@@ -501,7 +501,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
{
// Client, that was buffering, resumed playback but did not update others in time.
delayTicks = context.GetHighestPing() * 2 * TimeSpan.TicksPerMillisecond;
delayTicks = Math.Max(delayTicks, context.DefaultPing);
delayTicks = Math.Max(delayTicks, TimeSpan.FromMilliseconds(context.DefaultPing).Ticks);
context.LastActivity = currentTime.AddTicks(delayTicks);
@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.SyncPlay.GroupStates;
using MediaBrowser.Controller.SyncPlay.PlaybackRequests;
using MediaBrowser.Controller.SyncPlay.Requests;
using MediaBrowser.Model.SyncPlay;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
using SyncPlayGroup = Emby.Server.Implementations.SyncPlay.Group;
namespace Jellyfin.Server.Implementations.Tests.SyncPlay;
public class WaitingGroupStateTests
{
[Fact]
public void Ready_ClientResumedWithLowPing_AppliesTheDefaultPingFloorInMilliseconds()
{
var harness = new GroupHarness();
var group = harness.Group;
// Both members report a ping well under the default, so the floor is what decides the delay.
group.UpdatePing(harness.First, 10);
group.UpdatePing(harness.Second, 10);
group.PositionTicks = TimeSpan.FromMinutes(5).Ticks;
group.LastActivity = DateTime.UtcNow;
group.SetBuffering(harness.First, true);
group.SetBuffering(harness.Second, false);
var state = new WaitingGroupState(NullLoggerFactory.Instance) { ResumePlaying = true };
var before = DateTime.UtcNow;
state.HandleRequest(
new ReadyGroupRequest(DateTime.UtcNow, group.PositionTicks, true, harness.PlaylistItemId),
group,
GroupStateType.Waiting,
harness.First,
CancellationToken.None);
// DefaultPing is expressed in milliseconds, so the floor must be converted before being
// compared against a tick count. Without the conversion the floor is 500 ticks (0.05 ms)
// and never applies.
var scheduledDelay = group.LastActivity - before;
Assert.True(
scheduledDelay >= TimeSpan.FromMilliseconds(group.DefaultPing),
$"expected a resume delay of at least {group.DefaultPing} ms, got {scheduledDelay.TotalMilliseconds} ms");
}
private sealed class GroupHarness
{
public GroupHarness()
{
var userManager = new Mock<IUserManager>();
var sessionManager = new Mock<ISessionManager>();
var libraryManager = new Mock<ILibraryManager>();
var user = new User("tester", "auth-provider", "pwdreset-provider");
userManager.Setup(m => m.GetUserById(It.IsAny<Guid>())).Returns(user);
var item = new Mock<BaseItem>();
item.Setup(i => i.IsVisibleStandalone(It.IsAny<User>())).Returns(true);
item.Object.RunTimeTicks = TimeSpan.FromHours(2).Ticks;
libraryManager.Setup(m => m.GetItemById(It.IsAny<Guid>())).Returns(item.Object);
sessionManager
.Setup(m => m.SendSyncPlayCommand(It.IsAny<string>(), It.IsAny<SendCommand>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
Group = new SyncPlayGroup(
NullLoggerFactory.Instance,
userManager.Object,
sessionManager.Object,
libraryManager.Object);
First = new SessionInfo(sessionManager.Object, NullLogger.Instance)
{
Id = "first",
UserId = user.Id,
UserName = "first"
};
Second = new SessionInfo(sessionManager.Object, NullLogger.Instance)
{
Id = "second",
UserId = user.Id,
UserName = "second"
};
Group.CreateGroup(First, new NewGroupRequest("group"), CancellationToken.None);
Group.SessionJoin(Second, new JoinGroupRequest(Group.GroupId), CancellationToken.None);
Group.SetPlayQueue(new List<Guid> { Guid.NewGuid() }, 0, 0);
PlaylistItemId = Group.PlayQueue.GetPlayingItemPlaylistId();
}
public SyncPlayGroup Group { get; }
public SessionInfo First { get; }
public SessionInfo Second { get; }
public Guid PlaylistItemId { get; }
}
}