Group.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. #nullable disable
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using Jellyfin.Data.Entities;
  8. using MediaBrowser.Controller.Library;
  9. using MediaBrowser.Controller.Session;
  10. using MediaBrowser.Controller.SyncPlay;
  11. using MediaBrowser.Controller.SyncPlay.GroupStates;
  12. using MediaBrowser.Controller.SyncPlay.Queue;
  13. using MediaBrowser.Controller.SyncPlay.Requests;
  14. using MediaBrowser.Model.SyncPlay;
  15. using Microsoft.Extensions.Logging;
  16. namespace Emby.Server.Implementations.SyncPlay
  17. {
  18. /// <summary>
  19. /// Class Group.
  20. /// </summary>
  21. /// <remarks>
  22. /// Class is not thread-safe, external locking is required when accessing methods.
  23. /// </remarks>
  24. public class Group : IGroupStateContext
  25. {
  26. /// <summary>
  27. /// The logger.
  28. /// </summary>
  29. private readonly ILogger<Group> _logger;
  30. /// <summary>
  31. /// The logger factory.
  32. /// </summary>
  33. private readonly ILoggerFactory _loggerFactory;
  34. /// <summary>
  35. /// The user manager.
  36. /// </summary>
  37. private readonly IUserManager _userManager;
  38. /// <summary>
  39. /// The session manager.
  40. /// </summary>
  41. private readonly ISessionManager _sessionManager;
  42. /// <summary>
  43. /// The library manager.
  44. /// </summary>
  45. private readonly ILibraryManager _libraryManager;
  46. /// <summary>
  47. /// The participants, or members of the group.
  48. /// </summary>
  49. private readonly Dictionary<string, GroupMember> _participants =
  50. new Dictionary<string, GroupMember>(StringComparer.OrdinalIgnoreCase);
  51. /// <summary>
  52. /// The internal group state.
  53. /// </summary>
  54. private IGroupState _state;
  55. /// <summary>
  56. /// Initializes a new instance of the <see cref="Group" /> class.
  57. /// </summary>
  58. /// <param name="loggerFactory">The logger factory.</param>
  59. /// <param name="userManager">The user manager.</param>
  60. /// <param name="sessionManager">The session manager.</param>
  61. /// <param name="libraryManager">The library manager.</param>
  62. public Group(
  63. ILoggerFactory loggerFactory,
  64. IUserManager userManager,
  65. ISessionManager sessionManager,
  66. ILibraryManager libraryManager)
  67. {
  68. _loggerFactory = loggerFactory;
  69. _userManager = userManager;
  70. _sessionManager = sessionManager;
  71. _libraryManager = libraryManager;
  72. _logger = loggerFactory.CreateLogger<Group>();
  73. _state = new IdleGroupState(loggerFactory);
  74. }
  75. /// <summary>
  76. /// Gets the default ping value used for sessions.
  77. /// </summary>
  78. /// <value>The default ping.</value>
  79. public long DefaultPing { get; } = 500;
  80. /// <summary>
  81. /// Gets the maximum time offset error accepted for dates reported by clients, in milliseconds.
  82. /// </summary>
  83. /// <value>The maximum time offset error.</value>
  84. public long TimeSyncOffset { get; } = 2000;
  85. /// <summary>
  86. /// Gets the maximum offset error accepted for position reported by clients, in milliseconds.
  87. /// </summary>
  88. /// <value>The maximum offset error.</value>
  89. public long MaxPlaybackOffset { get; } = 500;
  90. /// <summary>
  91. /// Gets the group identifier.
  92. /// </summary>
  93. /// <value>The group identifier.</value>
  94. public Guid GroupId { get; } = Guid.NewGuid();
  95. /// <summary>
  96. /// Gets the group name.
  97. /// </summary>
  98. /// <value>The group name.</value>
  99. public string GroupName { get; private set; }
  100. /// <summary>
  101. /// Gets the group identifier.
  102. /// </summary>
  103. /// <value>The group identifier.</value>
  104. public PlayQueueManager PlayQueue { get; } = new PlayQueueManager();
  105. /// <summary>
  106. /// Gets the runtime ticks of current playing item.
  107. /// </summary>
  108. /// <value>The runtime ticks of current playing item.</value>
  109. public long RunTimeTicks { get; private set; }
  110. /// <summary>
  111. /// Gets or sets the position ticks.
  112. /// </summary>
  113. /// <value>The position ticks.</value>
  114. public long PositionTicks { get; set; }
  115. /// <summary>
  116. /// Gets or sets the last activity.
  117. /// </summary>
  118. /// <value>The last activity.</value>
  119. public DateTime LastActivity { get; set; }
  120. /// <summary>
  121. /// Adds the session to the group.
  122. /// </summary>
  123. /// <param name="session">The session.</param>
  124. private void AddSession(SessionInfo session)
  125. {
  126. _participants.TryAdd(
  127. session.Id,
  128. new GroupMember(session)
  129. {
  130. Ping = DefaultPing,
  131. IsBuffering = false
  132. });
  133. }
  134. /// <summary>
  135. /// Removes the session from the group.
  136. /// </summary>
  137. /// <param name="session">The session.</param>
  138. private void RemoveSession(SessionInfo session)
  139. {
  140. _participants.Remove(session.Id);
  141. }
  142. /// <summary>
  143. /// Filters sessions of this group.
  144. /// </summary>
  145. /// <param name="fromId">The current session identifier.</param>
  146. /// <param name="type">The filtering type.</param>
  147. /// <returns>The list of sessions matching the filter.</returns>
  148. private IEnumerable<string> FilterSessions(string fromId, SyncPlayBroadcastType type)
  149. {
  150. return type switch
  151. {
  152. SyncPlayBroadcastType.CurrentSession => new string[] { fromId },
  153. SyncPlayBroadcastType.AllGroup => _participants
  154. .Values
  155. .Select(member => member.SessionId),
  156. SyncPlayBroadcastType.AllExceptCurrentSession => _participants
  157. .Values
  158. .Select(member => member.SessionId)
  159. .Where(sessionId => !sessionId.Equals(fromId, StringComparison.OrdinalIgnoreCase)),
  160. SyncPlayBroadcastType.AllReady => _participants
  161. .Values
  162. .Where(member => !member.IsBuffering)
  163. .Select(member => member.SessionId),
  164. _ => Enumerable.Empty<string>()
  165. };
  166. }
  167. /// <summary>
  168. /// Checks if a given user can access all items of a given queue, that is,
  169. /// the user has the required minimum parental access and has access to all required folders.
  170. /// </summary>
  171. /// <param name="user">The user.</param>
  172. /// <param name="queue">The queue.</param>
  173. /// <returns><c>true</c> if the user can access all the items in the queue, <c>false</c> otherwise.</returns>
  174. private bool HasAccessToQueue(User user, IReadOnlyList<Guid> queue)
  175. {
  176. // Check if queue is empty.
  177. if (queue == null || queue.Count == 0)
  178. {
  179. return true;
  180. }
  181. foreach (var itemId in queue)
  182. {
  183. var item = _libraryManager.GetItemById(itemId);
  184. if (!item.IsVisibleStandalone(user))
  185. {
  186. return false;
  187. }
  188. }
  189. return true;
  190. }
  191. private bool AllUsersHaveAccessToQueue(IReadOnlyList<Guid> queue)
  192. {
  193. // Check if queue is empty.
  194. if (queue == null || queue.Count == 0)
  195. {
  196. return true;
  197. }
  198. // Get list of users.
  199. var users = _participants
  200. .Values
  201. .Select(participant => _userManager.GetUserById(participant.UserId));
  202. // Find problematic users.
  203. var usersWithNoAccess = users.Where(user => !HasAccessToQueue(user, queue));
  204. // All users must be able to access the queue.
  205. return !usersWithNoAccess.Any();
  206. }
  207. /// <summary>
  208. /// Checks if the group is empty.
  209. /// </summary>
  210. /// <returns><c>true</c> if the group is empty, <c>false</c> otherwise.</returns>
  211. public bool IsGroupEmpty() => _participants.Count == 0;
  212. /// <summary>
  213. /// Initializes the group with the session's info.
  214. /// </summary>
  215. /// <param name="session">The session.</param>
  216. /// <param name="request">The request.</param>
  217. /// <param name="cancellationToken">The cancellation token.</param>
  218. public void CreateGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken)
  219. {
  220. GroupName = request.GroupName;
  221. AddSession(session);
  222. var sessionIsPlayingAnItem = session.FullNowPlayingItem != null;
  223. RestartCurrentItem();
  224. if (sessionIsPlayingAnItem)
  225. {
  226. var playlist = session.NowPlayingQueue.Select(item => item.Id).ToList();
  227. PlayQueue.Reset();
  228. PlayQueue.SetPlaylist(playlist);
  229. PlayQueue.SetPlayingItemById(session.FullNowPlayingItem.Id);
  230. RunTimeTicks = session.FullNowPlayingItem.RunTimeTicks ?? 0;
  231. PositionTicks = session.PlayState.PositionTicks ?? 0;
  232. // Maintain playstate.
  233. var waitingState = new WaitingGroupState(_loggerFactory)
  234. {
  235. ResumePlaying = !session.PlayState.IsPaused
  236. };
  237. SetState(waitingState);
  238. }
  239. var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, GetInfo());
  240. SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
  241. _state.SessionJoined(this, _state.Type, session, cancellationToken);
  242. _logger.LogInformation("Session {SessionId} created group {GroupId}.", session.Id, GroupId.ToString());
  243. }
  244. /// <summary>
  245. /// Adds the session to the group.
  246. /// </summary>
  247. /// <param name="session">The session.</param>
  248. /// <param name="request">The request.</param>
  249. /// <param name="cancellationToken">The cancellation token.</param>
  250. public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
  251. {
  252. AddSession(session);
  253. var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, GetInfo());
  254. SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
  255. var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
  256. SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
  257. _state.SessionJoined(this, _state.Type, session, cancellationToken);
  258. _logger.LogInformation("Session {SessionId} joined group {GroupId}.", session.Id, GroupId.ToString());
  259. }
  260. /// <summary>
  261. /// Removes the session from the group.
  262. /// </summary>
  263. /// <param name="session">The session.</param>
  264. /// <param name="request">The request.</param>
  265. /// <param name="cancellationToken">The cancellation token.</param>
  266. public void SessionLeave(SessionInfo session, LeaveGroupRequest request, CancellationToken cancellationToken)
  267. {
  268. _state.SessionLeaving(this, _state.Type, session, cancellationToken);
  269. RemoveSession(session);
  270. var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupLeft, GroupId.ToString());
  271. SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken);
  272. var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserLeft, session.UserName);
  273. SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
  274. _logger.LogInformation("Session {SessionId} left group {GroupId}.", session.Id, GroupId.ToString());
  275. }
  276. /// <summary>
  277. /// Handles the requested action by the session.
  278. /// </summary>
  279. /// <param name="session">The session.</param>
  280. /// <param name="request">The requested action.</param>
  281. /// <param name="cancellationToken">The cancellation token.</param>
  282. public void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToken)
  283. {
  284. // The server's job is to maintain a consistent state for clients to reference
  285. // and notify clients of state changes. The actual syncing of media playback
  286. // happens client side. Clients are aware of the server's time and use it to sync.
  287. _logger.LogInformation("Session {SessionId} requested {RequestType} in group {GroupId} that is {StateType}.", session.Id, request.Action, GroupId.ToString(), _state.Type);
  288. // Apply requested changes to this group given its current state.
  289. // Every request has a slightly different outcome depending on the group's state.
  290. // There are currently four different group states that accomplish different goals:
  291. // - Idle: in this state no media is playing and clients should be idle (playback is stopped).
  292. // - Waiting: in this state the group is waiting for all the clients to be ready to start the playback,
  293. // that is, they've either finished loading the media for the first time or they've finished buffering.
  294. // Once all clients report to be ready the group's state can change to Playing or Paused.
  295. // - Playing: clients have some media loaded and playback is unpaused.
  296. // - Paused: clients have some media loaded but playback is currently paused.
  297. request.Apply(this, _state, session, cancellationToken);
  298. }
  299. /// <summary>
  300. /// Gets the info about the group for the clients.
  301. /// </summary>
  302. /// <returns>The group info for the clients.</returns>
  303. public GroupInfoDto GetInfo()
  304. {
  305. var participants = _participants.Values.Select(session => session.UserName).Distinct().ToList();
  306. return new GroupInfoDto(GroupId, GroupName, _state.Type, participants, DateTime.UtcNow);
  307. }
  308. /// <summary>
  309. /// Checks if a user has access to all content in the play queue.
  310. /// </summary>
  311. /// <param name="user">The user.</param>
  312. /// <returns><c>true</c> if the user can access the play queue; <c>false</c> otherwise.</returns>
  313. public bool HasAccessToPlayQueue(User user)
  314. {
  315. var items = PlayQueue.GetPlaylist().Select(item => item.ItemId).ToList();
  316. return HasAccessToQueue(user, items);
  317. }
  318. /// <inheritdoc />
  319. public void SetIgnoreGroupWait(SessionInfo session, bool ignoreGroupWait)
  320. {
  321. if (_participants.TryGetValue(session.Id, out GroupMember value))
  322. {
  323. value.IgnoreGroupWait = ignoreGroupWait;
  324. }
  325. }
  326. /// <inheritdoc />
  327. public void SetState(IGroupState state)
  328. {
  329. _logger.LogInformation("Group {GroupId} switching from {FromStateType} to {ToStateType}.", GroupId.ToString(), _state.Type, state.Type);
  330. this._state = state;
  331. }
  332. /// <inheritdoc />
  333. public Task SendGroupUpdate<T>(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken)
  334. {
  335. IEnumerable<Task> GetTasks()
  336. {
  337. foreach (var sessionId in FilterSessions(from.Id, type))
  338. {
  339. yield return _sessionManager.SendSyncPlayGroupUpdate(sessionId, message, cancellationToken);
  340. }
  341. }
  342. return Task.WhenAll(GetTasks());
  343. }
  344. /// <inheritdoc />
  345. public Task SendCommand(SessionInfo from, SyncPlayBroadcastType type, SendCommand message, CancellationToken cancellationToken)
  346. {
  347. IEnumerable<Task> GetTasks()
  348. {
  349. foreach (var sessionId in FilterSessions(from.Id, type))
  350. {
  351. yield return _sessionManager.SendSyncPlayCommand(sessionId, message, cancellationToken);
  352. }
  353. }
  354. return Task.WhenAll(GetTasks());
  355. }
  356. /// <inheritdoc />
  357. public SendCommand NewSyncPlayCommand(SendCommandType type)
  358. {
  359. return new SendCommand(
  360. GroupId,
  361. PlayQueue.GetPlayingItemPlaylistId(),
  362. LastActivity,
  363. type,
  364. PositionTicks,
  365. DateTime.UtcNow);
  366. }
  367. /// <inheritdoc />
  368. public GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data)
  369. {
  370. return new GroupUpdate<T>(GroupId, type, data);
  371. }
  372. /// <inheritdoc />
  373. public long SanitizePositionTicks(long? positionTicks)
  374. {
  375. var ticks = positionTicks ?? 0;
  376. return Math.Clamp(ticks, 0, RunTimeTicks);
  377. }
  378. /// <inheritdoc />
  379. public void UpdatePing(SessionInfo session, long ping)
  380. {
  381. if (_participants.TryGetValue(session.Id, out GroupMember value))
  382. {
  383. value.Ping = ping;
  384. }
  385. }
  386. /// <inheritdoc />
  387. public long GetHighestPing()
  388. {
  389. long max = long.MinValue;
  390. foreach (var session in _participants.Values)
  391. {
  392. max = Math.Max(max, session.Ping);
  393. }
  394. return max;
  395. }
  396. /// <inheritdoc />
  397. public void SetBuffering(SessionInfo session, bool isBuffering)
  398. {
  399. if (_participants.TryGetValue(session.Id, out GroupMember value))
  400. {
  401. value.IsBuffering = isBuffering;
  402. }
  403. }
  404. /// <inheritdoc />
  405. public void SetAllBuffering(bool isBuffering)
  406. {
  407. foreach (var session in _participants.Values)
  408. {
  409. session.IsBuffering = isBuffering;
  410. }
  411. }
  412. /// <inheritdoc />
  413. public bool IsBuffering()
  414. {
  415. foreach (var session in _participants.Values)
  416. {
  417. if (session.IsBuffering && !session.IgnoreGroupWait)
  418. {
  419. return true;
  420. }
  421. }
  422. return false;
  423. }
  424. /// <inheritdoc />
  425. public bool SetPlayQueue(IReadOnlyList<Guid> playQueue, int playingItemPosition, long startPositionTicks)
  426. {
  427. // Ignore on empty queue or invalid item position.
  428. if (playQueue.Count == 0 || playingItemPosition >= playQueue.Count || playingItemPosition < 0)
  429. {
  430. return false;
  431. }
  432. // Check if participants can access the new playing queue.
  433. if (!AllUsersHaveAccessToQueue(playQueue))
  434. {
  435. return false;
  436. }
  437. PlayQueue.Reset();
  438. PlayQueue.SetPlaylist(playQueue);
  439. PlayQueue.SetPlayingItemByIndex(playingItemPosition);
  440. var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId());
  441. RunTimeTicks = item.RunTimeTicks ?? 0;
  442. PositionTicks = startPositionTicks;
  443. LastActivity = DateTime.UtcNow;
  444. return true;
  445. }
  446. /// <inheritdoc />
  447. public bool SetPlayingItem(Guid playlistItemId)
  448. {
  449. var itemFound = PlayQueue.SetPlayingItemByPlaylistId(playlistItemId);
  450. if (itemFound)
  451. {
  452. var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId());
  453. RunTimeTicks = item.RunTimeTicks ?? 0;
  454. }
  455. else
  456. {
  457. RunTimeTicks = 0;
  458. }
  459. RestartCurrentItem();
  460. return itemFound;
  461. }
  462. /// <inheritdoc />
  463. public void ClearPlayQueue(bool clearPlayingItem)
  464. {
  465. PlayQueue.ClearPlaylist(clearPlayingItem);
  466. if (clearPlayingItem)
  467. {
  468. RestartCurrentItem();
  469. }
  470. }
  471. /// <inheritdoc />
  472. public bool RemoveFromPlayQueue(IReadOnlyList<Guid> playlistItemIds)
  473. {
  474. var playingItemRemoved = PlayQueue.RemoveFromPlaylist(playlistItemIds);
  475. if (playingItemRemoved)
  476. {
  477. var itemId = PlayQueue.GetPlayingItemId();
  478. if (!itemId.Equals(default))
  479. {
  480. var item = _libraryManager.GetItemById(itemId);
  481. RunTimeTicks = item.RunTimeTicks ?? 0;
  482. }
  483. else
  484. {
  485. RunTimeTicks = 0;
  486. }
  487. RestartCurrentItem();
  488. }
  489. return playingItemRemoved;
  490. }
  491. /// <inheritdoc />
  492. public bool MoveItemInPlayQueue(Guid playlistItemId, int newIndex)
  493. {
  494. return PlayQueue.MovePlaylistItem(playlistItemId, newIndex);
  495. }
  496. /// <inheritdoc />
  497. public bool AddToPlayQueue(IReadOnlyList<Guid> newItems, GroupQueueMode mode)
  498. {
  499. // Ignore on empty list.
  500. if (newItems.Count == 0)
  501. {
  502. return false;
  503. }
  504. // Check if participants can access the new playing queue.
  505. if (!AllUsersHaveAccessToQueue(newItems))
  506. {
  507. return false;
  508. }
  509. if (mode.Equals(GroupQueueMode.QueueNext))
  510. {
  511. PlayQueue.QueueNext(newItems);
  512. }
  513. else
  514. {
  515. PlayQueue.Queue(newItems);
  516. }
  517. return true;
  518. }
  519. /// <inheritdoc />
  520. public void RestartCurrentItem()
  521. {
  522. PositionTicks = 0;
  523. LastActivity = DateTime.UtcNow;
  524. }
  525. /// <inheritdoc />
  526. public bool NextItemInQueue()
  527. {
  528. var update = PlayQueue.Next();
  529. if (update)
  530. {
  531. var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId());
  532. RunTimeTicks = item.RunTimeTicks ?? 0;
  533. RestartCurrentItem();
  534. return true;
  535. }
  536. else
  537. {
  538. return false;
  539. }
  540. }
  541. /// <inheritdoc />
  542. public bool PreviousItemInQueue()
  543. {
  544. var update = PlayQueue.Previous();
  545. if (update)
  546. {
  547. var item = _libraryManager.GetItemById(PlayQueue.GetPlayingItemId());
  548. RunTimeTicks = item.RunTimeTicks ?? 0;
  549. RestartCurrentItem();
  550. return true;
  551. }
  552. else
  553. {
  554. return false;
  555. }
  556. }
  557. /// <inheritdoc />
  558. public void SetRepeatMode(GroupRepeatMode mode)
  559. {
  560. PlayQueue.SetRepeatMode(mode);
  561. }
  562. /// <inheritdoc />
  563. public void SetShuffleMode(GroupShuffleMode mode)
  564. {
  565. PlayQueue.SetShuffleMode(mode);
  566. }
  567. /// <inheritdoc />
  568. public PlayQueueUpdate GetPlayQueueUpdate(PlayQueueUpdateReason reason)
  569. {
  570. var startPositionTicks = PositionTicks;
  571. var isPlaying = _state.Type.Equals(GroupStateType.Playing);
  572. if (isPlaying)
  573. {
  574. var currentTime = DateTime.UtcNow;
  575. var elapsedTime = currentTime - LastActivity;
  576. // Elapsed time is negative if event happens
  577. // during the delay added to account for latency.
  578. // In this phase clients haven't started the playback yet.
  579. // In other words, LastActivity is in the future,
  580. // when playback unpause is supposed to happen.
  581. // Adjust ticks only if playback actually started.
  582. startPositionTicks += Math.Max(elapsedTime.Ticks, 0);
  583. }
  584. return new PlayQueueUpdate(
  585. reason,
  586. PlayQueue.LastChange,
  587. PlayQueue.GetPlaylist(),
  588. PlayQueue.PlayingItemIndex,
  589. startPositionTicks,
  590. isPlaying,
  591. PlayQueue.ShuffleMode,
  592. PlayQueue.RepeatMode);
  593. }
  594. }
  595. }