< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.SyncPlay.Group
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/SyncPlay/Group.cs
Line coverage
74%
Covered lines: 193
Uncovered lines: 65
Coverable lines: 258
Total lines: 813
Line coverage: 74.8%
Branch coverage
58%
Covered branches: 50
Total branches: 85
Branch coverage: 58.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/12/2026 - 12:15:57 AM Line coverage: 0% (0/210) Branch coverage: 0% (0/63) Total lines: 6796/19/2026 - 12:16:12 AM Line coverage: 10% (21/210) Branch coverage: 12.3% (8/65) Total lines: 6807/3/2026 - 12:15:32 AM Line coverage: 34.7% (73/210) Branch coverage: 16.9% (11/65) Total lines: 68010/29/2026 - 12:17:11 AM Line coverage: 74.8% (193/258) Branch coverage: 58.8% (50/85) Total lines: 813 5/12/2026 - 12:15:57 AM Line coverage: 0% (0/210) Branch coverage: 0% (0/63) Total lines: 6796/19/2026 - 12:16:12 AM Line coverage: 10% (21/210) Branch coverage: 12.3% (8/65) Total lines: 6807/3/2026 - 12:15:32 AM Line coverage: 34.7% (73/210) Branch coverage: 16.9% (11/65) Total lines: 68010/29/2026 - 12:17:11 AM Line coverage: 74.8% (193/258) Branch coverage: 58.8% (50/85) Total lines: 813

Coverage delta

Coverage delta 42 -42

Metrics

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/SyncPlay/Group.cs

#LineLine coverage
 1#nullable disable
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Linq;
 6using System.Threading;
 7using System.Threading.Tasks;
 8using Jellyfin.Database.Implementations.Entities;
 9using Jellyfin.Extensions;
 10using MediaBrowser.Controller.Library;
 11using MediaBrowser.Controller.Session;
 12using MediaBrowser.Controller.SyncPlay;
 13using MediaBrowser.Controller.SyncPlay.GroupStates;
 14using MediaBrowser.Controller.SyncPlay.PlaybackRequests;
 15using MediaBrowser.Controller.SyncPlay.Queue;
 16using MediaBrowser.Controller.SyncPlay.Requests;
 17using MediaBrowser.Model.SyncPlay;
 18using Microsoft.Extensions.Logging;
 19
 20namespace Emby.Server.Implementations.SyncPlay
 21{
 22    /// <summary>
 23    /// Class Group.
 24    /// </summary>
 25    /// <remarks>
 26    /// Class is not thread-safe, external locking is required when accessing methods.
 27    /// </remarks>
 28    public class Group : IGroupStateContext
 29    {
 30        /// <summary>
 31        /// The default value of <see cref="GroupWaitTimeout"/>, in milliseconds.
 32        /// </summary>
 33        internal const long DefaultGroupWaitTimeout = 30000;
 34
 35        /// <summary>
 36        /// The logger.
 37        /// </summary>
 38        private readonly ILogger<Group> _logger;
 39
 40        /// <summary>
 41        /// The logger factory.
 42        /// </summary>
 43        private readonly ILoggerFactory _loggerFactory;
 44
 45        /// <summary>
 46        /// The user manager.
 47        /// </summary>
 48        private readonly IUserManager _userManager;
 49
 50        /// <summary>
 51        /// The session manager.
 52        /// </summary>
 53        private readonly ISessionManager _sessionManager;
 54
 55        /// <summary>
 56        /// The library manager.
 57        /// </summary>
 58        private readonly ILibraryManager _libraryManager;
 59
 60        /// <summary>
 61        /// The participants, or members of the group.
 62        /// </summary>
 2063        private readonly Dictionary<string, GroupMember> _participants = new(StringComparer.OrdinalIgnoreCase);
 64
 65        /// <summary>
 66        /// The sessions of the participants, which only carry identifiers.
 67        /// </summary>
 2068        private readonly Dictionary<string, SessionInfo> _participantSessions = new(StringComparer.OrdinalIgnoreCase);
 69
 70        /// <summary>
 71        /// The internal group state.
 72        /// </summary>
 73        private IGroupState _state;
 74
 75        /// <summary>
 76        /// Initializes a new instance of the <see cref="Group" /> class.
 77        /// </summary>
 78        /// <param name="loggerFactory">The logger factory.</param>
 79        /// <param name="userManager">The user manager.</param>
 80        /// <param name="sessionManager">The session manager.</param>
 81        /// <param name="libraryManager">The library manager.</param>
 82        public Group(
 83            ILoggerFactory loggerFactory,
 84            IUserManager userManager,
 85            ISessionManager sessionManager,
 86            ILibraryManager libraryManager)
 87        {
 2088            _loggerFactory = loggerFactory;
 2089            _userManager = userManager;
 2090            _sessionManager = sessionManager;
 2091            _libraryManager = libraryManager;
 2092            _logger = loggerFactory.CreateLogger<Group>();
 93
 2094            _state = new IdleGroupState(loggerFactory);
 2095        }
 96
 97        /// <summary>
 98        /// Gets the default ping value used for sessions.
 99        /// </summary>
 100        /// <value>The default ping.</value>
 20101        public long DefaultPing { get; } = 500;
 102
 103        /// <summary>
 104        /// Gets the maximum ping, in milliseconds, accepted from a session.
 105        /// </summary>
 106        /// <remarks>
 107        /// Pings are reported by clients and are scaled into the delays used to schedule playback,
 108        /// so an unbounded value lets a single session push the whole group's resume point
 109        /// arbitrarily far out, or overflow the arithmetic entirely. Anything above this is not a
 110        /// usable measurement for synchronisation.
 111        /// </remarks>
 112        /// <value>The maximum ping.</value>
 20113        public long MaxPing { get; } = 10000;
 114
 115        /// <summary>
 116        /// Gets the maximum time offset error accepted for dates reported by clients, in milliseconds.
 117        /// </summary>
 118        /// <value>The maximum time offset error.</value>
 20119        public long TimeSyncOffset { get; } = 2000;
 120
 121        /// <summary>
 122        /// Gets the maximum offset error accepted for position reported by clients, in milliseconds.
 123        /// </summary>
 124        /// <value>The maximum offset error.</value>
 20125        public long MaxPlaybackOffset { get; } = 500;
 126
 127        /// <summary>
 128        /// Gets the maximum time, in milliseconds, the group waits for its members to report ready.
 129        /// </summary>
 130        /// <value>The group-wait timeout.</value>
 20131        internal long GroupWaitTimeout { get; init; } = DefaultGroupWaitTimeout;
 132
 133        /// <summary>
 134        /// Gets the <see cref="Environment.TickCount64"/> value at which the group gives up waiting
 135        /// for its members, or <c>null</c> when it is not waiting for anyone.
 136        /// </summary>
 137        /// <value>The group-wait deadline.</value>
 138        internal long? GroupWaitDeadline { get; private set; }
 139
 140        /// <summary>
 141        /// Gets the group identifier.
 142        /// </summary>
 143        /// <value>The group identifier.</value>
 144        public Guid GroupId { get; } = Guid.NewGuid();
 145
 146        /// <summary>
 147        /// Gets the group name.
 148        /// </summary>
 149        /// <value>The group name.</value>
 150        public string GroupName { get; private set; }
 151
 152        /// <summary>
 153        /// Gets the group identifier.
 154        /// </summary>
 155        /// <value>The group identifier.</value>
 156        public PlayQueueManager PlayQueue { get; } = new PlayQueueManager();
 157
 158        /// <summary>
 159        /// Gets the runtime ticks of current playing item.
 160        /// </summary>
 161        /// <value>The runtime ticks of current playing item.</value>
 162        public long RunTimeTicks { get; private set; }
 163
 164        /// <summary>
 165        /// Gets or sets the position ticks.
 166        /// </summary>
 167        /// <value>The position ticks.</value>
 168        public long PositionTicks { get; set; }
 169
 170        /// <summary>
 171        /// Gets or sets the last activity.
 172        /// </summary>
 173        /// <value>The last activity.</value>
 174        public DateTime LastActivity { get; set; }
 175
 176        /// <summary>
 177        /// Adds the session to the group.
 178        /// </summary>
 179        /// <param name="session">The session.</param>
 180        private void AddSession(SessionInfo session)
 181        {
 38182            _participants.TryAdd(
 38183                session.Id,
 38184                new GroupMember(session)
 38185                {
 38186                    Ping = DefaultPing,
 38187                    IsBuffering = false
 38188                });
 189
 38190            _participantSessions[session.Id] = session;
 38191        }
 192
 193        /// <summary>
 194        /// Removes the session from the group.
 195        /// </summary>
 196        /// <param name="session">The session.</param>
 197        private void RemoveSession(SessionInfo session)
 198        {
 5199            _participants.Remove(session.Id);
 5200            _participantSessions.Remove(session.Id);
 5201            UpdateGroupWaitDeadline(false);
 5202        }
 203
 204        /// <summary>
 205        /// Filters sessions of this group.
 206        /// </summary>
 207        /// <param name="fromId">The current session identifier.</param>
 208        /// <param name="type">The filtering type.</param>
 209        /// <returns>The list of sessions matching the filter.</returns>
 210        private IEnumerable<string> FilterSessions(string fromId, SyncPlayBroadcastType type)
 211        {
 143212            return type switch
 143213            {
 84214                SyncPlayBroadcastType.CurrentSession => new string[] { fromId },
 28215                SyncPlayBroadcastType.AllGroup => _participants
 28216                    .Values
 28217                    .Select(member => member.SessionId),
 26218                SyncPlayBroadcastType.AllExceptCurrentSession => _participants
 26219                    .Values
 26220                    .Select(member => member.SessionId)
 26221                    .Where(sessionId => !sessionId.Equals(fromId, StringComparison.OrdinalIgnoreCase)),
 5222                SyncPlayBroadcastType.AllReady => _participants
 5223                    .Values
 5224                    .Where(member => !member.IsBuffering)
 5225                    .Select(member => member.SessionId),
 0226                _ => Enumerable.Empty<string>()
 143227            };
 228        }
 229
 230        /// <summary>
 231        /// Checks if a given user can access all items of a given queue, that is,
 232        /// the user has the required minimum parental access and has access to all required folders.
 233        /// </summary>
 234        /// <param name="user">The user.</param>
 235        /// <param name="queue">The queue.</param>
 236        /// <returns><c>true</c> if the user can access all the items in the queue, <c>false</c> otherwise.</returns>
 237        private bool HasAccessToQueue(User user, IReadOnlyList<Guid> queue)
 238        {
 239            // Check if queue is empty.
 105240            if (queue is null || queue.Count == 0)
 241            {
 25242                return true;
 243            }
 244
 319245            foreach (var itemId in queue)
 246            {
 80247                var item = _libraryManager.GetItemById(itemId);
 248
 80249                if (item is null || !item.IsVisibleStandalone(user))
 250                {
 1251                    return false;
 252                }
 253            }
 254
 79255            return true;
 1256        }
 257
 258        private bool AllUsersHaveAccessToQueue(IReadOnlyList<Guid> queue)
 259        {
 260            // Check if queue is empty.
 14261            if (queue is null || queue.Count == 0)
 262            {
 0263                return true;
 264            }
 265
 266            // Get list of users.
 14267            var users = _participants
 14268                .Values
 14269                .Select(participant => _userManager.GetUserById(participant.UserId));
 270
 271            // Find problematic users.
 14272            var usersWithNoAccess = users.Where(user => !HasAccessToQueue(user, queue));
 273
 274            // All users must be able to access the queue.
 14275            return !usersWithNoAccess.Any();
 276        }
 277
 278        /// <summary>
 279        /// Checks if the group is empty.
 280        /// </summary>
 281        /// <returns><c>true</c> if the group is empty, <c>false</c> otherwise.</returns>
 6282        public bool IsGroupEmpty() => _participants.Count == 0;
 283
 284        /// <summary>
 285        /// Initializes the group with the session's info.
 286        /// </summary>
 287        /// <param name="session">The session.</param>
 288        /// <param name="request">The request.</param>
 289        /// <param name="cancellationToken">The cancellation token.</param>
 290        public void CreateGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken)
 291        {
 18292            GroupName = request.GroupName;
 18293            AddSession(session);
 294
 18295            var sessionIsPlayingAnItem = session.FullNowPlayingItem is not null;
 296
 18297            RestartCurrentItem();
 298
 18299            if (sessionIsPlayingAnItem)
 300            {
 0301                var playlist = session.NowPlayingQueue.Select(item => item.Id).ToList();
 0302                PlayQueue.Reset();
 0303                PlayQueue.SetPlaylist(playlist);
 0304                PlayQueue.SetPlayingItemById(session.FullNowPlayingItem.Id);
 0305                RunTimeTicks = session.FullNowPlayingItem.RunTimeTicks ?? 0;
 0306                PositionTicks = session.PlayState.PositionTicks ?? 0;
 307
 308                // Maintain playstate.
 0309                var waitingState = new WaitingGroupState(_loggerFactory)
 0310                {
 0311                    ResumePlaying = !session.PlayState.IsPaused
 0312                };
 0313                SetState(waitingState);
 314            }
 315
 18316            var updateSession = new SyncPlayGroupJoinedUpdate(GroupId, GetInfo());
 18317            SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
 318
 18319            _state.SessionJoined(this, _state.Type, session, cancellationToken);
 320
 18321            _logger.LogInformation("Session {SessionId} created group {GroupId}.", session.Id, GroupId.ToString());
 18322        }
 323
 324        /// <summary>
 325        /// Adds the session to the group.
 326        /// </summary>
 327        /// <param name="session">The session.</param>
 328        /// <param name="request">The request.</param>
 329        /// <param name="cancellationToken">The cancellation token.</param>
 330        public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
 331        {
 20332            AddSession(session);
 333
 20334            var updateSession = new SyncPlayGroupJoinedUpdate(GroupId, GetInfo());
 20335            SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
 336
 20337            var updateOthers = new SyncPlayUserJoinedUpdate(GroupId, session.UserName);
 20338            SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
 339
 20340            _state.SessionJoined(this, _state.Type, session, cancellationToken);
 341
 20342            _logger.LogInformation("Session {SessionId} joined group {GroupId}.", session.Id, GroupId.ToString());
 20343        }
 344
 345        /// <summary>
 346        /// Removes the session from the group.
 347        /// </summary>
 348        /// <param name="session">The session.</param>
 349        /// <param name="request">The request.</param>
 350        /// <param name="cancellationToken">The cancellation token.</param>
 351        public void SessionLeave(SessionInfo session, LeaveGroupRequest request, CancellationToken cancellationToken)
 352        {
 5353            _state.SessionLeaving(this, _state.Type, session, cancellationToken);
 354
 5355            RemoveSession(session);
 356
 5357            var updateSession = new SyncPlayGroupLeftUpdate(GroupId, GroupId.ToString());
 5358            SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
 359
 5360            var updateOthers = new SyncPlayUserLeftUpdate(GroupId, session.UserName);
 5361            SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
 362
 5363            _logger.LogInformation("Session {SessionId} left group {GroupId}.", session.Id, GroupId.ToString());
 5364        }
 365
 366        /// <summary>
 367        /// Handles the requested action by the session.
 368        /// </summary>
 369        /// <param name="session">The session.</param>
 370        /// <param name="request">The requested action.</param>
 371        /// <param name="cancellationToken">The cancellation token.</param>
 372        public void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToke
 373        {
 374            // The server's job is to maintain a consistent state for clients to reference
 375            // and notify clients of state changes. The actual syncing of media playback
 376            // happens client side. Clients are aware of the server's time and use it to sync.
 4377            _logger.LogInformation("Session {SessionId} requested {RequestType} in group {GroupId} that is {StateType}."
 378
 379            // Apply requested changes to this group given its current state.
 380            // Every request has a slightly different outcome depending on the group's state.
 381            // There are currently four different group states that accomplish different goals:
 382            // - Idle: in this state no media is playing and clients should be idle (playback is stopped).
 383            // - Waiting: in this state the group is waiting for all the clients to be ready to start the playback,
 384            //      that is, they've either finished loading the media for the first time or they've finished buffering.
 385            //      Once all clients report to be ready the group's state can change to Playing or Paused.
 386            // - Playing: clients have some media loaded and playback is unpaused.
 387            // - Paused: clients have some media loaded but playback is currently paused.
 4388            request.Apply(this, _state, session, cancellationToken);
 4389        }
 390
 391        /// <summary>
 392        /// Gets the info about the group for the clients.
 393        /// </summary>
 394        /// <returns>The group info for the clients.</returns>
 395        public GroupInfoDto GetInfo()
 396        {
 161397            var participants = _participants.Values.Select(session => session.UserName).Distinct().ToList();
 161398            return new GroupInfoDto(GroupId, GroupName, _state.Type, participants, DateTime.UtcNow);
 399        }
 400
 401        /// <summary>
 402        /// Checks if a user has access to all content in the play queue.
 403        /// </summary>
 404        /// <param name="user">The user.</param>
 405        /// <returns><c>true</c> if the user can access the play queue; <c>false</c> otherwise.</returns>
 406        public bool HasAccessToPlayQueue(User user)
 407        {
 77408            var items = PlayQueue.GetPlaylist().Select(item => item.ItemId).ToList();
 77409            return HasAccessToQueue(user, items);
 410        }
 411
 412        /// <inheritdoc />
 413        public void SetIgnoreGroupWait(SessionInfo session, bool ignoreGroupWait)
 414        {
 0415            if (_participants.TryGetValue(session.Id, out GroupMember value))
 416            {
 0417                value.IgnoreGroupWait = ignoreGroupWait;
 418            }
 419
 0420            UpdateGroupWaitDeadline(false);
 0421        }
 422
 423        /// <inheritdoc />
 424        public void SetState(IGroupState state)
 425        {
 17426            _logger.LogInformation("Group {GroupId} switching from {FromStateType} to {ToStateType}.", GroupId.ToString(
 17427            _state = state;
 428
 17429            if (state.Type != GroupStateType.Waiting)
 430            {
 11431                GroupWaitDeadline = null;
 432            }
 17433        }
 434
 435        /// <inheritdoc />
 436        public Task SendGroupUpdate<T>(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate<T> message, Cancellatio
 437        {
 438            IEnumerable<Task> GetTasks()
 439            {
 440                foreach (var sessionId in FilterSessions(from.Id, type))
 441                {
 442                    yield return _sessionManager.SendSyncPlayGroupUpdate(sessionId, message, cancellationToken);
 443                }
 444            }
 445
 89446            return Task.WhenAll(GetTasks());
 447        }
 448
 449        /// <inheritdoc />
 450        public Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken can
 451        {
 452            IEnumerable<Task> GetTasks()
 453            {
 454                foreach (var sessionId in FilterSessions(from.Id, type))
 455                {
 456                    yield return _sessionManager.SendSyncPlayCommand(sessionId, message, cancellationToken);
 457                }
 458            }
 459
 54460            return Task.WhenAll(GetTasks());
 461        }
 462
 463        /// <inheritdoc />
 464        public SendCommand NewSyncPlayCommand(SendCommandType type)
 465        {
 54466            return new SendCommand(
 54467                GroupId,
 54468                PlayQueue.GetPlayingItemPlaylistId(),
 54469                LastActivity,
 54470                type,
 54471                PositionTicks,
 54472                DateTime.UtcNow);
 473        }
 474
 475        /// <inheritdoc />
 476        public long SanitizePositionTicks(long? positionTicks)
 477        {
 7478            var ticks = positionTicks ?? 0;
 7479            return Math.Clamp(ticks, 0, RunTimeTicks);
 480        }
 481
 482        /// <inheritdoc />
 483        public void UpdatePing(SessionInfo session, long ping)
 484        {
 6485            if (_participants.TryGetValue(session.Id, out GroupMember value))
 486            {
 6487                value.Ping = Math.Clamp(ping, 0, MaxPing);
 488            }
 6489        }
 490
 491        /// <inheritdoc />
 492        public long GetHighestPing()
 493        {
 16494            long max = long.MinValue;
 104495            foreach (var session in _participants.Values)
 496            {
 36497                max = Math.Max(max, session.Ping);
 498            }
 499
 500            // A group with no participants has no ping to report. Returning long.MinValue would
 501            // overflow the callers that scale this value into ticks, so fall back to the default.
 16502            return max == long.MinValue ? DefaultPing : max;
 503        }
 504
 505        /// <inheritdoc />
 506        public void SetBuffering(SessionInfo session, bool isBuffering)
 507        {
 17508            if (_participants.TryGetValue(session.Id, out GroupMember value))
 509            {
 17510                value.IsBuffering = isBuffering;
 511            }
 512
 17513            UpdateGroupWaitDeadline(false);
 17514        }
 515
 516        /// <inheritdoc />
 517        public void SetAllBuffering(bool isBuffering)
 518        {
 50519            foreach (var session in _participants.Values)
 520            {
 18521                session.IsBuffering = isBuffering;
 522            }
 523
 524            // Resetting the status of every session starts a new waiting period.
 7525            UpdateGroupWaitDeadline(isBuffering);
 7526        }
 527
 528        /// <inheritdoc />
 529        public bool IsBuffering()
 530        {
 100531            foreach (var session in _participants.Values)
 532            {
 38533                if (session.IsBuffering && !session.IgnoreGroupWait)
 534                {
 8535                    return true;
 536                }
 537            }
 538
 8539            return false;
 8540        }
 541
 542        /// <inheritdoc />
 543        public bool SetPlayQueue(IReadOnlyList<Guid> playQueue, int playingItemPosition, long startPositionTicks)
 544        {
 545            // Ignore on empty queue or invalid item position.
 14546            if (playQueue.Count == 0 || playingItemPosition >= playQueue.Count || playingItemPosition < 0)
 547            {
 0548                return false;
 549            }
 550
 551            // Check if participants can access the new playing queue.
 14552            if (!AllUsersHaveAccessToQueue(playQueue))
 553            {
 0554                return false;
 555            }
 556
 14557            PlayQueue.Reset();
 14558            PlayQueue.SetPlaylist(playQueue);
 14559            PlayQueue.SetPlayingItemByIndex(playingItemPosition);
 14560            var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId());
 14561            RunTimeTicks = item.RunTimeTicks ?? 0;
 14562            PositionTicks = startPositionTicks;
 14563            LastActivity = DateTime.UtcNow;
 564
 14565            return true;
 566        }
 567
 568        /// <inheritdoc />
 569        public bool SetPlayingItem(Guid playlistItemId)
 570        {
 1571            var itemFound = PlayQueue.SetPlayingItemByPlaylistId(playlistItemId);
 572
 1573            if (itemFound)
 574            {
 1575                var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId());
 1576                RunTimeTicks = item.RunTimeTicks ?? 0;
 577            }
 578            else
 579            {
 0580                RunTimeTicks = 0;
 581            }
 582
 1583            RestartCurrentItem();
 584
 1585            return itemFound;
 586        }
 587
 588        /// <inheritdoc />
 589        public void ClearPlayQueue(bool clearPlayingItem)
 590        {
 0591            PlayQueue.ClearPlaylist(clearPlayingItem);
 0592            if (clearPlayingItem)
 593            {
 0594                RestartCurrentItem();
 595            }
 0596        }
 597
 598        /// <inheritdoc />
 599        public bool RemoveFromPlayQueue(IReadOnlyList<Guid> playlistItemIds)
 600        {
 0601            var playingItemRemoved = PlayQueue.RemoveFromPlaylist(playlistItemIds);
 0602            if (playingItemRemoved)
 603            {
 0604                var itemId = PlayQueue.GetPlayingItemId();
 0605                if (!itemId.IsEmpty())
 606                {
 0607                    var item = _libraryManager.GetItemById(itemId);
 0608                    RunTimeTicks = item.RunTimeTicks ?? 0;
 609                }
 610                else
 611                {
 0612                    RunTimeTicks = 0;
 613                }
 614
 0615                RestartCurrentItem();
 616            }
 617
 0618            return playingItemRemoved;
 619        }
 620
 621        /// <inheritdoc />
 622        public bool MoveItemInPlayQueue(Guid playlistItemId, int newIndex)
 623        {
 0624            return PlayQueue.MovePlaylistItem(playlistItemId, newIndex);
 625        }
 626
 627        /// <inheritdoc />
 628        public bool AddToPlayQueue(IReadOnlyList<Guid> newItems, GroupQueueMode mode)
 629        {
 630            // Ignore on empty list.
 0631            if (newItems.Count == 0)
 632            {
 0633                return false;
 634            }
 635
 636            // Check if participants can access the new playing queue.
 0637            if (!AllUsersHaveAccessToQueue(newItems))
 638            {
 0639                return false;
 640            }
 641
 0642            if (mode.Equals(GroupQueueMode.QueueNext))
 643            {
 0644                PlayQueue.QueueNext(newItems);
 645            }
 646            else
 647            {
 0648                PlayQueue.Queue(newItems);
 649            }
 650
 0651            return true;
 652        }
 653
 654        /// <inheritdoc />
 655        public void RestartCurrentItem()
 656        {
 19657            PositionTicks = 0;
 19658            LastActivity = DateTime.UtcNow;
 19659        }
 660
 661        /// <inheritdoc />
 662        public bool NextItemInQueue()
 663        {
 0664            var update = PlayQueue.Next();
 0665            if (update)
 666            {
 0667                var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId());
 0668                RunTimeTicks = item.RunTimeTicks ?? 0;
 0669                RestartCurrentItem();
 0670                return true;
 671            }
 672
 0673            return false;
 674        }
 675
 676        /// <inheritdoc />
 677        public bool PreviousItemInQueue()
 678        {
 0679            var update = PlayQueue.Previous();
 0680            if (update)
 681            {
 0682                var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId());
 0683                RunTimeTicks = item.RunTimeTicks ?? 0;
 0684                RestartCurrentItem();
 0685                return true;
 686            }
 687
 0688            return false;
 689        }
 690
 691        /// <inheritdoc />
 692        public void SetRepeatMode(GroupRepeatMode mode)
 693        {
 0694            PlayQueue.SetRepeatMode(mode);
 0695        }
 696
 697        /// <inheritdoc />
 698        public void SetShuffleMode(GroupShuffleMode mode)
 699        {
 0700            PlayQueue.SetShuffleMode(mode);
 0701        }
 702
 703        /// <inheritdoc />
 704        public PlayQueueUpdate GetPlayQueueUpdate(PlayQueueUpdateReason reason)
 705        {
 6706            var startPositionTicks = PositionTicks;
 6707            var isPlaying = _state.Type.Equals(GroupStateType.Playing);
 708
 6709            if (isPlaying)
 710            {
 0711                var currentTime = DateTime.UtcNow;
 0712                var elapsedTime = currentTime - LastActivity;
 713                // Elapsed time is negative if event happens
 714                // during the delay added to account for latency.
 715                // In this phase clients haven't started the playback yet.
 716                // In other words, LastActivity is in the future,
 717                // when playback unpause is supposed to happen.
 718                // Adjust ticks only if playback actually started.
 0719                startPositionTicks += Math.Max(elapsedTime.Ticks, 0);
 720            }
 721
 6722            return new PlayQueueUpdate(
 6723                reason,
 6724                PlayQueue.LastChange,
 6725                PlayQueue.GetPlaylist(),
 6726                PlayQueue.PlayingItemIndex,
 6727                startPositionTicks,
 6728                isPlaying,
 6729                PlayQueue.ShuffleMode,
 6730                PlayQueue.RepeatMode);
 731        }
 732
 733        /// <summary>
 734        /// Stops waiting for the members that have not reported ready and lets the rest of the
 735        /// group carry on. Does nothing until <see cref="GroupWaitDeadline"/> has passed.
 736        /// </summary>
 737        /// <param name="cancellationToken">The cancellation token.</param>
 738        internal void HandleGroupWaitTimeout(CancellationToken cancellationToken)
 739        {
 103740            var deadline = GroupWaitDeadline;
 103741            if (deadline is null || deadline > Environment.TickCount64)
 742            {
 99743                return;
 744            }
 745
 4746            GroupWaitDeadline = null;
 747
 4748            if (_state is not WaitingGroupState waitingState)
 749            {
 0750                return;
 751            }
 752
 4753            var blockingSessions = _participantSessions
 4754                .Values
 4755                .Where(participant => _participants.TryGetValue(participant.Id, out var member)
 4756                    && member.IsBuffering
 4757                    && !member.IgnoreGroupWait)
 4758                .ToList();
 759
 4760            if (blockingSessions.Count == 0)
 761            {
 0762                return;
 763            }
 764
 765            // The recovery below is broadcast to the whole group, so it does not matter which of
 766            // the sessions that kept the group waiting is the one acting on the group's behalf.
 4767            var session = blockingSessions[0];
 768
 4769            _logger.LogWarning(
 4770                "Group {GroupId} waited {Waited} ms for session(s) {SessionIds} to report ready, giving up.",
 4771                GroupId.ToString(),
 4772                GroupWaitTimeout + Environment.TickCount64 - deadline.Value,
 4773                string.Join(", ", blockingSessions.Select(participant => participant.Id)));
 774
 4775            if (waitingState.ResumePlaying)
 776            {
 777                // An unpause request in the waiting state means "start now, ignoring the sessions
 778                // that are not ready".
 3779                var unpauseRequest = new UnpauseGroupRequest();
 3780                waitingState.HandleRequest(unpauseRequest, this, GroupStateType.Waiting, session, cancellationToken);
 3781                return;
 782            }
 783
 784            // The members have been paused for the whole waiting period, so the playback position
 785            // stays where the wait started.
 1786            SetAllBuffering(false);
 1787            SetState(new PausedGroupState(_loggerFactory));
 788
 1789            var command = NewSyncPlayCommand(SendCommandType.Pause);
 1790            SendCommand(session, SyncPlayBroadcastType.AllGroup, command, cancellationToken);
 791
 1792            var stateUpdate = new GroupStateUpdate(GroupStateType.Paused, PlaybackRequestType.Pause);
 1793            var update = new SyncPlayStateUpdate(GroupId, stateUpdate);
 1794            SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken);
 1795        }
 796
 797        private void UpdateGroupWaitDeadline(bool startNewWaitingPeriod)
 798        {
 29799            if (_state.Type != GroupStateType.Waiting || !IsBuffering())
 800            {
 23801                GroupWaitDeadline = null;
 23802                return;
 803            }
 804
 805            // A running deadline covers the waiting period as a whole, so the sessions that keep
 806            // reporting buffering while they load must not push it back.
 6807            if (GroupWaitDeadline is null || startNewWaitingPeriod)
 808            {
 6809                GroupWaitDeadline = Environment.TickCount64 + GroupWaitTimeout;
 810            }
 6811        }
 812    }
 813}

Methods/Properties

.ctor(Microsoft.Extensions.Logging.ILoggerFactory,MediaBrowser.Controller.Library.IUserManager,MediaBrowser.Controller.Session.ISessionManager,MediaBrowser.Controller.Library.ILibraryManager)
AddSession(MediaBrowser.Controller.Session.SessionInfo)
RemoveSession(MediaBrowser.Controller.Session.SessionInfo)
FilterSessions(System.String,MediaBrowser.Model.SyncPlay.SyncPlayBroadcastType)
HasAccessToQueue(Jellyfin.Database.Implementations.Entities.User,System.Collections.Generic.IReadOnlyList`1<System.Guid>)
AllUsersHaveAccessToQueue(System.Collections.Generic.IReadOnlyList`1<System.Guid>)
IsGroupEmpty()
CreateGroup(MediaBrowser.Controller.Session.SessionInfo,MediaBrowser.Controller.SyncPlay.Requests.NewGroupRequest,System.Threading.CancellationToken)
SessionJoin(MediaBrowser.Controller.Session.SessionInfo,MediaBrowser.Controller.SyncPlay.Requests.JoinGroupRequest,System.Threading.CancellationToken)
SessionLeave(MediaBrowser.Controller.Session.SessionInfo,MediaBrowser.Controller.SyncPlay.Requests.LeaveGroupRequest,System.Threading.CancellationToken)
HandleRequest(MediaBrowser.Controller.Session.SessionInfo,MediaBrowser.Controller.SyncPlay.IGroupPlaybackRequest,System.Threading.CancellationToken)
GetInfo()
HasAccessToPlayQueue(Jellyfin.Database.Implementations.Entities.User)
SetIgnoreGroupWait(MediaBrowser.Controller.Session.SessionInfo,System.Boolean)
SetState(MediaBrowser.Controller.SyncPlay.IGroupState)
SendGroupUpdate(MediaBrowser.Controller.Session.SessionInfo,MediaBrowser.Model.SyncPlay.SyncPlayBroadcastType,MediaBrowser.Model.SyncPlay.GroupUpdate`1<T>,System.Threading.CancellationToken)
SendCommand(MediaBrowser.Controller.Session.SessionInfo,MediaBrowser.Model.SyncPlay.SyncPlayBroadcastType,MediaBrowser.Model.SyncPlay.SendCommand,System.Threading.CancellationToken)
NewSyncPlayCommand(MediaBrowser.Model.SyncPlay.SendCommandType)
SanitizePositionTicks(System.Nullable`1<System.Int64>)
UpdatePing(MediaBrowser.Controller.Session.SessionInfo,System.Int64)
GetHighestPing()
SetBuffering(MediaBrowser.Controller.Session.SessionInfo,System.Boolean)
SetAllBuffering(System.Boolean)
IsBuffering()
SetPlayQueue(System.Collections.Generic.IReadOnlyList`1<System.Guid>,System.Int32,System.Int64)
SetPlayingItem(System.Guid)
ClearPlayQueue(System.Boolean)
RemoveFromPlayQueue(System.Collections.Generic.IReadOnlyList`1<System.Guid>)
MoveItemInPlayQueue(System.Guid,System.Int32)
AddToPlayQueue(System.Collections.Generic.IReadOnlyList`1<System.Guid>,MediaBrowser.Model.SyncPlay.GroupQueueMode)
RestartCurrentItem()
NextItemInQueue()
PreviousItemInQueue()
SetRepeatMode(MediaBrowser.Model.SyncPlay.GroupRepeatMode)
SetShuffleMode(MediaBrowser.Model.SyncPlay.GroupShuffleMode)
GetPlayQueueUpdate(MediaBrowser.Model.SyncPlay.PlayQueueUpdateReason)
HandleGroupWaitTimeout(System.Threading.CancellationToken)
UpdateGroupWaitDeadline(System.Boolean)