gion 5 роки тому
батько
коміт
6e22e9222b

+ 2 - 2
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -238,10 +238,10 @@ namespace Emby.Server.Implementations.HttpServer
             return _socket.SendAsync(text, true, cancellationToken);
             return _socket.SendAsync(text, true, cancellationToken);
         }
         }
 
 
-        private Task SendKeepAliveResponse()
+        private void SendKeepAliveResponse()
         {
         {
             LastKeepAliveDate = DateTime.UtcNow;
             LastKeepAliveDate = DateTime.UtcNow;
-            return SendAsync(new WebSocketMessage<string>
+            SendAsync(new WebSocketMessage<string>
             {
             {
                 MessageType = "KeepAlive"
                 MessageType = "KeepAlive"
             }, CancellationToken.None);
             }, CancellationToken.None);

+ 69 - 58
Emby.Server.Implementations/Session/SessionWebSocketListener.cs

@@ -84,10 +84,10 @@ namespace Emby.Server.Implementations.Session
             _logger = loggerFactory.CreateLogger(GetType().Name);
             _logger = loggerFactory.CreateLogger(GetType().Name);
             _json = json;
             _json = json;
             _httpServer = httpServer;
             _httpServer = httpServer;
-            httpServer.WebSocketConnected += _serverManager_WebSocketConnected;
+            httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
         }
         }
 
 
-        void _serverManager_WebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
+        void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
         {
         {
             var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint);
             var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint);
 
 
@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Session
 
 
         public void Dispose()
         public void Dispose()
         {
         {
-            _httpServer.WebSocketConnected -= _serverManager_WebSocketConnected;
+            _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
             StopKeepAlive();
             StopKeepAlive();
         }
         }
 
 
@@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.Session
         private void OnWebSocketClosed(object sender, EventArgs e)
         private void OnWebSocketClosed(object sender, EventArgs e)
         {
         {
             var webSocket = (IWebSocketConnection) sender;
             var webSocket = (IWebSocketConnection) sender;
-            _logger.LogDebug("WebSockets {0} closed.", webSocket);
+            _logger.LogDebug("WebSocket {0} is closed.", webSocket);
             RemoveWebSocket(webSocket);
             RemoveWebSocket(webSocket);
         }
         }
 
 
@@ -157,7 +157,7 @@ namespace Emby.Server.Implementations.Session
         /// Adds a WebSocket to the KeepAlive watchlist.
         /// Adds a WebSocket to the KeepAlive watchlist.
         /// </summary>
         /// </summary>
         /// <param name="webSocket">The WebSocket to monitor.</param>
         /// <param name="webSocket">The WebSocket to monitor.</param>
-        private async Task KeepAliveWebSocket(IWebSocketConnection webSocket)
+        private void KeepAliveWebSocket(IWebSocketConnection webSocket)
         {
         {
             lock (_webSocketsLock)
             lock (_webSocketsLock)
             {
             {
@@ -175,11 +175,11 @@ namespace Emby.Server.Implementations.Session
             // Notify WebSocket about timeout
             // Notify WebSocket about timeout
             try
             try
             {
             {
-                await SendForceKeepAlive(webSocket);
+                SendForceKeepAlive(webSocket).Wait();
             }
             }
             catch (WebSocketException exception)
             catch (WebSocketException exception)
             {
             {
-                _logger.LogWarning(exception, "Error sending ForceKeepAlive message to WebSocket {0}.", webSocket);
+                _logger.LogWarning(exception, "Cannot send ForceKeepAlive message to WebSocket {0}.", webSocket);
             }
             }
         }
         }
 
 
@@ -213,7 +213,8 @@ namespace Emby.Server.Implementations.Session
                 {
                 {
                     _keepAliveCancellationToken = new CancellationTokenSource();
                     _keepAliveCancellationToken = new CancellationTokenSource();
                     // Start KeepAlive watcher
                     // Start KeepAlive watcher
-                    KeepAliveSockets(
+                    var task = RepeatAsyncCallbackEvery(
+                        KeepAliveSockets,
                         TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor),
                         TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor),
                         _keepAliveCancellationToken.Token);
                         _keepAliveCancellationToken.Token);
                 }
                 }
@@ -245,73 +246,58 @@ namespace Emby.Server.Implementations.Session
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Checks status of KeepAlive of WebSockets once every the specified interval time.
+        /// Checks status of KeepAlive of WebSockets.
         /// </summary>
         /// </summary>
-        /// <param name="interval">The interval.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        private async Task KeepAliveSockets(TimeSpan interval, CancellationToken cancellationToken)
+        private async Task KeepAliveSockets()
         {
         {
-            while (true)
+            IEnumerable<IWebSocketConnection> inactive;
+            IEnumerable<IWebSocketConnection> lost;
+
+            lock (_webSocketsLock)
             {
             {
-                _logger.LogDebug("Watching {0} WebSockets.", _webSockets.Count());
+                _logger.LogDebug("Watching {0} WebSockets.", _webSockets.Count);
 
 
-                IEnumerable<IWebSocketConnection> inactive;
-                IEnumerable<IWebSocketConnection> lost;
-                lock (_webSocketsLock)
+                inactive = _webSockets.Where(i =>
                 {
                 {
-                    inactive = _webSockets.Where(i =>
-                    {
-                        var elapsed = (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds;
-                        return (elapsed > WebSocketLostTimeout * ForceKeepAliveFactor) && (elapsed < WebSocketLostTimeout);
-                    });
-                    lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout);
-                }
+                    var elapsed = (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds;
+                    return (elapsed > WebSocketLostTimeout * ForceKeepAliveFactor) && (elapsed < WebSocketLostTimeout);
+                });
+                lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout);
+            }
 
 
-                if (inactive.Any())
+            if (inactive.Any())
+            {
+                _logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count());
+            }
+
+            foreach (var webSocket in inactive)
+            {
+                try
                 {
                 {
-                    _logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count());
+                    await SendForceKeepAlive(webSocket);
                 }
                 }
-
-                foreach (var webSocket in inactive)
+                catch (WebSocketException exception)
                 {
                 {
-                    try
-                    {
-                        await SendForceKeepAlive(webSocket);
-                    }
-                    catch (WebSocketException exception)
-                    {
-                        _logger.LogInformation(exception, "Error sending ForceKeepAlive message to WebSocket.");
-                        lost = lost.Append(webSocket);
-                    }
+                    _logger.LogInformation(exception, "Error sending ForceKeepAlive message to WebSocket.");
+                    lost = lost.Append(webSocket);
                 }
                 }
+            }
 
 
-                lock (_webSocketsLock)
+            lock (_webSocketsLock)
+            {
+                if (lost.Any())
                 {
                 {
-                    if (lost.Any())
-                    {
-                        _logger.LogInformation("Lost {0} WebSockets.", lost.Count());
-                        foreach (var webSocket in lost.ToList())
-                        {
-                            // TODO: handle session relative to the lost webSocket
-                            RemoveWebSocket(webSocket);
-                        }
-                    }
-
-                    if (!_webSockets.Any())
+                    _logger.LogInformation("Lost {0} WebSockets.", lost.Count());
+                    foreach (var webSocket in lost.ToList())
                     {
                     {
-                        StopKeepAlive();
+                        // TODO: handle session relative to the lost webSocket
+                        RemoveWebSocket(webSocket);
                     }
                     }
                 }
                 }
 
 
-                // Wait for next interval
-                Task task = Task.Delay(interval, cancellationToken);
-                try
+                if (!_webSockets.Any())
                 {
                 {
-                    await task;
-                }
-                catch (TaskCanceledException)
-                {
-                    return;
+                    StopKeepAlive();
                 }
                 }
             }
             }
         }
         }
@@ -329,5 +315,30 @@ namespace Emby.Server.Implementations.Session
                 Data = WebSocketLostTimeout
                 Data = WebSocketLostTimeout
             }, CancellationToken.None);
             }, CancellationToken.None);
         }
         }
+
+        /// <summary>
+        /// Runs a given async callback once every specified interval time, until cancelled.
+        /// </summary>
+        /// <param name="callback">The async callback.</param>
+        /// <param name="interval">The interval time.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        private async Task RepeatAsyncCallbackEvery(Func<Task> callback, TimeSpan interval, CancellationToken cancellationToken)
+        {
+            while (!cancellationToken.IsCancellationRequested)
+            {
+                await callback();
+                Task task = Task.Delay(interval, cancellationToken);
+
+                try
+                {
+                    await task;
+                }
+                catch (TaskCanceledException)
+                {
+                    return;
+                }
+            }
+        }
     }
     }
 }
 }

+ 91 - 67
Emby.Server.Implementations/Syncplay/SyncplayController.cs

@@ -7,13 +7,15 @@ using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Syncplay;
 using MediaBrowser.Controller.Syncplay;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Syncplay;
 using MediaBrowser.Model.Syncplay;
-using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.Syncplay
 namespace Emby.Server.Implementations.Syncplay
 {
 {
     /// <summary>
     /// <summary>
     /// Class SyncplayController.
     /// Class SyncplayController.
     /// </summary>
     /// </summary>
+    /// <remarks>
+    /// Class is not thread-safe, external locking is required when accessing methods.
+    /// </remarks>
     public class SyncplayController : ISyncplayController, IDisposable
     public class SyncplayController : ISyncplayController, IDisposable
     {
     {
         /// <summary>
         /// <summary>
@@ -39,11 +41,6 @@ namespace Emby.Server.Implementations.Syncplay
             AllReady = 3
             AllReady = 3
         }
         }
 
 
-        /// <summary>
-        /// The logger.
-        /// </summary>
-        private readonly ILogger _logger;
-
         /// <summary>
         /// <summary>
         /// The session manager.
         /// The session manager.
         /// </summary>
         /// </summary>
@@ -71,11 +68,9 @@ namespace Emby.Server.Implementations.Syncplay
         private bool _disposed = false;
         private bool _disposed = false;
 
 
         public SyncplayController(
         public SyncplayController(
-            ILogger logger,
             ISessionManager sessionManager,
             ISessionManager sessionManager,
             ISyncplayManager syncplayManager)
             ISyncplayManager syncplayManager)
         {
         {
-            _logger = logger;
             _sessionManager = sessionManager;
             _sessionManager = sessionManager;
             _syncplayManager = syncplayManager;
             _syncplayManager = syncplayManager;
         }
         }
@@ -110,6 +105,16 @@ namespace Emby.Server.Implementations.Syncplay
             }
             }
         }
         }
 
 
+        /// <summary>
+        /// Converts DateTime to UTC string.
+        /// </summary>
+        /// <param name="date">The date to convert.</param>
+        /// <value>The UTC string.</value>
+        private string DateToUTCString(DateTime date)
+        {
+            return date.ToUniversalTime().ToString("o");
+        }
+
         /// <summary>
         /// <summary>
         /// Filters sessions of this group.
         /// Filters sessions of this group.
         /// </summary>
         /// </summary>
@@ -149,15 +154,16 @@ namespace Emby.Server.Implementations.Syncplay
         /// <param name="from">The current session.</param>
         /// <param name="from">The current session.</param>
         /// <param name="type">The filtering type.</param>
         /// <param name="type">The filtering type.</param>
         /// <param name="message">The message to send.</param>
         /// <param name="message">The message to send.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
         /// <value>The task.</value>
         /// <value>The task.</value>
-        private Task SendGroupUpdate<T>(SessionInfo from, BroadcastType type, GroupUpdate<T> message)
+        private Task SendGroupUpdate<T>(SessionInfo from, BroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken)
         {
         {
             IEnumerable<Task> GetTasks()
             IEnumerable<Task> GetTasks()
             {
             {
                 SessionInfo[] sessions = FilterSessions(from, type);
                 SessionInfo[] sessions = FilterSessions(from, type);
                 foreach (var session in sessions)
                 foreach (var session in sessions)
                 {
                 {
-                    yield return _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), message, CancellationToken.None);
+                    yield return _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), message, cancellationToken);
                 }
                 }
             }
             }
 
 
@@ -170,15 +176,16 @@ namespace Emby.Server.Implementations.Syncplay
         /// <param name="from">The current session.</param>
         /// <param name="from">The current session.</param>
         /// <param name="type">The filtering type.</param>
         /// <param name="type">The filtering type.</param>
         /// <param name="message">The message to send.</param>
         /// <param name="message">The message to send.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
         /// <value>The task.</value>
         /// <value>The task.</value>
-        private Task SendCommand(SessionInfo from, BroadcastType type, SendCommand message)
+        private Task SendCommand(SessionInfo from, BroadcastType type, SendCommand message, CancellationToken cancellationToken)
         {
         {
             IEnumerable<Task> GetTasks()
             IEnumerable<Task> GetTasks()
             {
             {
                 SessionInfo[] sessions = FilterSessions(from, type);
                 SessionInfo[] sessions = FilterSessions(from, type);
                 foreach (var session in sessions)
                 foreach (var session in sessions)
                 {
                 {
-                    yield return _sessionManager.SendSyncplayCommand(session.Id.ToString(), message, CancellationToken.None);
+                    yield return _sessionManager.SendSyncplayCommand(session.Id.ToString(), message, cancellationToken);
                 }
                 }
             }
             }
 
 
@@ -197,8 +204,8 @@ namespace Emby.Server.Implementations.Syncplay
                 GroupId = _group.GroupId.ToString(),
                 GroupId = _group.GroupId.ToString(),
                 Command = type,
                 Command = type,
                 PositionTicks = _group.PositionTicks,
                 PositionTicks = _group.PositionTicks,
-                When = _group.LastActivity.ToUniversalTime().ToString("o"),
-                EmittedAt = DateTime.UtcNow.ToUniversalTime().ToString("o")
+                When = DateToUTCString(_group.LastActivity),
+                EmittedAt = DateToUTCString(DateTime.UtcNow)
             };
             };
         }
         }
 
 
@@ -219,46 +226,46 @@ namespace Emby.Server.Implementations.Syncplay
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public void InitGroup(SessionInfo session)
+        public void InitGroup(SessionInfo session, CancellationToken cancellationToken)
         {
         {
             _group.AddSession(session);
             _group.AddSession(session);
             _syncplayManager.AddSessionToGroup(session, this);
             _syncplayManager.AddSessionToGroup(session, this);
 
 
             _group.PlayingItem = session.FullNowPlayingItem;
             _group.PlayingItem = session.FullNowPlayingItem;
             _group.IsPaused = true;
             _group.IsPaused = true;
-            _group.PositionTicks = session.PlayState.PositionTicks ??= 0;
+            _group.PositionTicks = session.PlayState.PositionTicks ?? 0;
             _group.LastActivity = DateTime.UtcNow;
             _group.LastActivity = DateTime.UtcNow;
 
 
-            var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupJoined, DateTime.UtcNow.ToUniversalTime().ToString("o"));
-            SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession);
+            var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow));
+            SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
             var pauseCommand = NewSyncplayCommand(SendCommandType.Pause);
             var pauseCommand = NewSyncplayCommand(SendCommandType.Pause);
-            SendCommand(session, BroadcastType.CurrentSession, pauseCommand);
+            SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken);
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public void SessionJoin(SessionInfo session, JoinGroupRequest request)
+        public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
         {
         {
             if (session.NowPlayingItem?.Id == _group.PlayingItem.Id && request.PlayingItemId == _group.PlayingItem.Id)
             if (session.NowPlayingItem?.Id == _group.PlayingItem.Id && request.PlayingItemId == _group.PlayingItem.Id)
             {
             {
                 _group.AddSession(session);
                 _group.AddSession(session);
                 _syncplayManager.AddSessionToGroup(session, this);
                 _syncplayManager.AddSessionToGroup(session, this);
 
 
-                var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupJoined, DateTime.UtcNow.ToUniversalTime().ToString("o"));
-                SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession);
+                var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow));
+                SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
 
 
                 var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
                 var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
-                SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers);
+                SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
 
 
                 // Client join and play, syncing will happen client side
                 // Client join and play, syncing will happen client side
                 if (!_group.IsPaused)
                 if (!_group.IsPaused)
                 {
                 {
                     var playCommand = NewSyncplayCommand(SendCommandType.Play);
                     var playCommand = NewSyncplayCommand(SendCommandType.Play);
-                    SendCommand(session, BroadcastType.CurrentSession, playCommand);
+                    SendCommand(session, BroadcastType.CurrentSession, playCommand, cancellationToken);
                 }
                 }
                 else
                 else
                 {
                 {
                     var pauseCommand = NewSyncplayCommand(SendCommandType.Pause);
                     var pauseCommand = NewSyncplayCommand(SendCommandType.Pause);
-                    SendCommand(session, BroadcastType.CurrentSession, pauseCommand);
+                    SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken);
                 }
                 }
             }
             }
             else
             else
@@ -267,25 +274,25 @@ namespace Emby.Server.Implementations.Syncplay
                 playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id };
                 playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id };
                 playRequest.StartPositionTicks = _group.PositionTicks;
                 playRequest.StartPositionTicks = _group.PositionTicks;
                 var update = NewSyncplayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
                 var update = NewSyncplayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
-                SendGroupUpdate(session, BroadcastType.CurrentSession, update);
+                SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken);
             }
             }
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public void SessionLeave(SessionInfo session)
+        public void SessionLeave(SessionInfo session, CancellationToken cancellationToken)
         {
         {
             _group.RemoveSession(session);
             _group.RemoveSession(session);
             _syncplayManager.RemoveSessionFromGroup(session, this);
             _syncplayManager.RemoveSessionFromGroup(session, this);
 
 
             var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupLeft, _group.PositionTicks);
             var updateSession = NewSyncplayGroupUpdate(GroupUpdateType.GroupLeft, _group.PositionTicks);
-            SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession);
+            SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
 
 
             var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.UserLeft, session.UserName);
             var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.UserLeft, session.UserName);
-            SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers);
+            SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public void HandleRequest(SessionInfo session, PlaybackRequest request)
+        public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
         {
         {
             // The server's job is to mantain a consistent state to which clients refer to,
             // The server's job is to mantain a consistent state to which clients refer to,
             // as also to notify clients of state changes.
             // as also to notify clients of state changes.
@@ -294,19 +301,19 @@ namespace Emby.Server.Implementations.Syncplay
             switch (request.Type)
             switch (request.Type)
             {
             {
                 case PlaybackRequestType.Play:
                 case PlaybackRequestType.Play:
-                    HandlePlayRequest(session, request);
+                    HandlePlayRequest(session, request, cancellationToken);
                     break;
                     break;
                 case PlaybackRequestType.Pause:
                 case PlaybackRequestType.Pause:
-                    HandlePauseRequest(session, request);
+                    HandlePauseRequest(session, request, cancellationToken);
                     break;
                     break;
                 case PlaybackRequestType.Seek:
                 case PlaybackRequestType.Seek:
-                    HandleSeekRequest(session, request);
+                    HandleSeekRequest(session, request, cancellationToken);
                     break;
                     break;
                 case PlaybackRequestType.Buffering:
                 case PlaybackRequestType.Buffering:
-                    HandleBufferingRequest(session, request);
+                    HandleBufferingRequest(session, request, cancellationToken);
                     break;
                     break;
                 case PlaybackRequestType.BufferingDone:
                 case PlaybackRequestType.BufferingDone:
-                    HandleBufferingDoneRequest(session, request);
+                    HandleBufferingDoneRequest(session, request, cancellationToken);
                     break;
                     break;
                 case PlaybackRequestType.UpdatePing:
                 case PlaybackRequestType.UpdatePing:
                     HandlePingUpdateRequest(session, request);
                     HandlePingUpdateRequest(session, request);
@@ -319,7 +326,8 @@ namespace Emby.Server.Implementations.Syncplay
         /// </summary>
         /// </summary>
         /// <param name="session">The session.</param>
         /// <param name="session">The session.</param>
         /// <param name="request">The play action.</param>
         /// <param name="request">The play action.</param>
-        private void HandlePlayRequest(SessionInfo session, PlaybackRequest request)
+        /// <param name="cancellationToken">The cancellation token.</param>
+        private void HandlePlayRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
         {
         {
             if (_group.IsPaused)
             if (_group.IsPaused)
             {
             {
@@ -337,13 +345,13 @@ namespace Emby.Server.Implementations.Syncplay
                 );
                 );
 
 
                 var command = NewSyncplayCommand(SendCommandType.Play);
                 var command = NewSyncplayCommand(SendCommandType.Play);
-                SendCommand(session, BroadcastType.AllGroup, command);
+                SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
             }
             }
             else
             else
             {
             {
                 // Client got lost, sending current state
                 // Client got lost, sending current state
                 var command = NewSyncplayCommand(SendCommandType.Play);
                 var command = NewSyncplayCommand(SendCommandType.Play);
-                SendCommand(session, BroadcastType.CurrentSession, command);
+                SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
             }
             }
         }
         }
 
 
@@ -352,7 +360,8 @@ namespace Emby.Server.Implementations.Syncplay
         /// </summary>
         /// </summary>
         /// <param name="session">The session.</param>
         /// <param name="session">The session.</param>
         /// <param name="request">The pause action.</param>
         /// <param name="request">The pause action.</param>
-        private void HandlePauseRequest(SessionInfo session, PlaybackRequest request)
+        /// <param name="cancellationToken">The cancellation token.</param>
+        private void HandlePauseRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
         {
         {
             if (!_group.IsPaused)
             if (!_group.IsPaused)
             {
             {
@@ -366,13 +375,13 @@ namespace Emby.Server.Implementations.Syncplay
                 _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
                 _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
 
 
                 var command = NewSyncplayCommand(SendCommandType.Pause);
                 var command = NewSyncplayCommand(SendCommandType.Pause);
-                SendCommand(session, BroadcastType.AllGroup, command);
+                SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
             }
             }
             else
             else
             {
             {
                 // Client got lost, sending current state
                 // Client got lost, sending current state
                 var command = NewSyncplayCommand(SendCommandType.Pause);
                 var command = NewSyncplayCommand(SendCommandType.Pause);
-                SendCommand(session, BroadcastType.CurrentSession, command);
+                SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
             }
             }
         }
         }
 
 
@@ -381,16 +390,11 @@ namespace Emby.Server.Implementations.Syncplay
         /// </summary>
         /// </summary>
         /// <param name="session">The session.</param>
         /// <param name="session">The session.</param>
         /// <param name="request">The seek action.</param>
         /// <param name="request">The seek action.</param>
-        private void HandleSeekRequest(SessionInfo session, PlaybackRequest request)
+        /// <param name="cancellationToken">The cancellation token.</param>
+        private void HandleSeekRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
         {
         {
             // Sanitize PositionTicks
             // Sanitize PositionTicks
-            var ticks = request.PositionTicks ??= 0;
-            ticks = ticks >= 0 ? ticks : 0;
-            if (_group.PlayingItem.RunTimeTicks != null)
-            {
-                var runTimeTicks = _group.PlayingItem.RunTimeTicks ??= 0;
-                ticks = ticks > runTimeTicks ? runTimeTicks : ticks;
-            }
+            var ticks = SanitizePositionTicks(request.PositionTicks);
 
 
             // Pause and seek
             // Pause and seek
             _group.IsPaused = true;
             _group.IsPaused = true;
@@ -398,7 +402,7 @@ namespace Emby.Server.Implementations.Syncplay
             _group.LastActivity = DateTime.UtcNow;
             _group.LastActivity = DateTime.UtcNow;
 
 
             var command = NewSyncplayCommand(SendCommandType.Seek);
             var command = NewSyncplayCommand(SendCommandType.Seek);
-            SendCommand(session, BroadcastType.AllGroup, command);
+            SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -406,7 +410,8 @@ namespace Emby.Server.Implementations.Syncplay
         /// </summary>
         /// </summary>
         /// <param name="session">The session.</param>
         /// <param name="session">The session.</param>
         /// <param name="request">The buffering action.</param>
         /// <param name="request">The buffering action.</param>
-        private void HandleBufferingRequest(SessionInfo session, PlaybackRequest request)
+        /// <param name="cancellationToken">The cancellation token.</param>
+        private void HandleBufferingRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
         {
         {
             if (!_group.IsPaused)
             if (!_group.IsPaused)
             {
             {
@@ -421,16 +426,16 @@ namespace Emby.Server.Implementations.Syncplay
 
 
                 // Send pause command to all non-buffering sessions
                 // Send pause command to all non-buffering sessions
                 var command = NewSyncplayCommand(SendCommandType.Pause);
                 var command = NewSyncplayCommand(SendCommandType.Pause);
-                SendCommand(session, BroadcastType.AllReady, command);
+                SendCommand(session, BroadcastType.AllReady, command, cancellationToken);
 
 
                 var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.GroupWait, session.UserName);
                 var updateOthers = NewSyncplayGroupUpdate(GroupUpdateType.GroupWait, session.UserName);
-                SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers);
+                SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
             }
             }
             else
             else
             {
             {
                 // Client got lost, sending current state
                 // Client got lost, sending current state
                 var command = NewSyncplayCommand(SendCommandType.Pause);
                 var command = NewSyncplayCommand(SendCommandType.Pause);
-                SendCommand(session, BroadcastType.CurrentSession, command);
+                SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
             }
             }
         }
         }
 
 
@@ -439,26 +444,28 @@ namespace Emby.Server.Implementations.Syncplay
         /// </summary>
         /// </summary>
         /// <param name="session">The session.</param>
         /// <param name="session">The session.</param>
         /// <param name="request">The buffering-done action.</param>
         /// <param name="request">The buffering-done action.</param>
-        private void HandleBufferingDoneRequest(SessionInfo session, PlaybackRequest request)
+        /// <param name="cancellationToken">The cancellation token.</param>
+        private void HandleBufferingDoneRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
         {
         {
             if (_group.IsPaused)
             if (_group.IsPaused)
             {
             {
                 _group.SetBuffering(session, false);
                 _group.SetBuffering(session, false);
 
 
-                var when = request.When ??= DateTime.UtcNow;
+                var requestTicks = SanitizePositionTicks(request.PositionTicks);
+
+                var when = request.When ?? DateTime.UtcNow;
                 var currentTime = DateTime.UtcNow;
                 var currentTime = DateTime.UtcNow;
                 var elapsedTime = currentTime - when;
                 var elapsedTime = currentTime - when;
-                var clientPosition = TimeSpan.FromTicks(request.PositionTicks ??= 0) + elapsedTime;
+                var clientPosition = TimeSpan.FromTicks(requestTicks) + elapsedTime;
                 var delay = _group.PositionTicks - clientPosition.Ticks;
                 var delay = _group.PositionTicks - clientPosition.Ticks;
 
 
                 if (_group.IsBuffering())
                 if (_group.IsBuffering())
                 {
                 {
-                    // Others are buffering, tell this client to pause when ready
+                    // Others are still buffering, tell this client to pause when ready
                     var command = NewSyncplayCommand(SendCommandType.Pause);
                     var command = NewSyncplayCommand(SendCommandType.Pause);
-                    command.When = currentTime.AddMilliseconds(
-                        delay
-                    ).ToUniversalTime().ToString("o");
-                    SendCommand(session, BroadcastType.CurrentSession, command);
+                    var pauseAtTime = currentTime.AddMilliseconds(delay);
+                    command.When = DateToUTCString(pauseAtTime);
+                    SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
                 }
                 }
                 else
                 else
                 {
                 {
@@ -472,7 +479,7 @@ namespace Emby.Server.Implementations.Syncplay
                             delay
                             delay
                         );
                         );
                         var command = NewSyncplayCommand(SendCommandType.Play);
                         var command = NewSyncplayCommand(SendCommandType.Play);
-                        SendCommand(session, BroadcastType.AllExceptCurrentSession, command);
+                        SendCommand(session, BroadcastType.AllExceptCurrentSession, command, cancellationToken);
                     }
                     }
                     else
                     else
                     {
                     {
@@ -485,7 +492,7 @@ namespace Emby.Server.Implementations.Syncplay
                         );
                         );
 
 
                         var command = NewSyncplayCommand(SendCommandType.Play);
                         var command = NewSyncplayCommand(SendCommandType.Play);
-                        SendCommand(session, BroadcastType.AllGroup, command);
+                        SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
                     }
                     }
                 }
                 }
             }
             }
@@ -493,8 +500,25 @@ namespace Emby.Server.Implementations.Syncplay
             {
             {
                 // Group was not waiting, make sure client has latest state
                 // Group was not waiting, make sure client has latest state
                 var command = NewSyncplayCommand(SendCommandType.Play);
                 var command = NewSyncplayCommand(SendCommandType.Play);
-                SendCommand(session, BroadcastType.CurrentSession, command);
+                SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
+            }
+        }
+
+        /// <summary>
+        /// Sanitizes the PositionTicks, considers the current playing item when available.
+        /// </summary>
+        /// <param name="positionTicks">The PositionTicks.</param>
+        /// <value>The sanitized PositionTicks.</value>
+        private long SanitizePositionTicks(long? positionTicks)
+        {
+            var ticks = positionTicks ?? 0;
+            ticks = ticks >= 0 ? ticks : 0;
+            if (_group.PlayingItem != null)
+            {
+                var runTimeTicks = _group.PlayingItem.RunTimeTicks ?? 0;
+                ticks = ticks > runTimeTicks ? runTimeTicks : ticks;
             }
             }
+            return ticks;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -505,7 +529,7 @@ namespace Emby.Server.Implementations.Syncplay
         private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request)
         private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request)
         {
         {
             // Collected pings are used to account for network latency when unpausing playback
             // Collected pings are used to account for network latency when unpausing playback
-            _group.UpdatePing(session, request.Ping ??= _group.DefaulPing);
+            _group.UpdatePing(session, request.Ping ?? _group.DefaulPing);
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
@@ -517,7 +541,7 @@ namespace Emby.Server.Implementations.Syncplay
                 PlayingItemName = _group.PlayingItem.Name,
                 PlayingItemName = _group.PlayingItem.Name,
                 PlayingItemId = _group.PlayingItem.Id.ToString(),
                 PlayingItemId = _group.PlayingItem.Id.ToString(),
                 PositionTicks = _group.PositionTicks,
                 PositionTicks = _group.PositionTicks,
-                Participants = _group.Participants.Values.Select(session => session.Session.UserName).ToArray()
+                Participants = _group.Participants.Values.Select(session => session.Session.UserName).Distinct().ToArray()
             };
             };
         }
         }
     }
     }

+ 34 - 27
Emby.Server.Implementations/Syncplay/SyncplayManager.cs

@@ -114,14 +114,14 @@ namespace Emby.Server.Implementations.Syncplay
         {
         {
             var session = e.SessionInfo;
             var session = e.SessionInfo;
             if (!IsSessionInGroup(session)) return;
             if (!IsSessionInGroup(session)) return;
-            LeaveGroup(session);
+            LeaveGroup(session, CancellationToken.None);
         }
         }
 
 
         private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e)
         private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e)
         {
         {
             var session = e.Session;
             var session = e.Session;
             if (!IsSessionInGroup(session)) return;
             if (!IsSessionInGroup(session)) return;
-            LeaveGroup(session);
+            LeaveGroup(session, CancellationToken.None);
         }
         }
 
 
         private bool IsSessionInGroup(SessionInfo session)
         private bool IsSessionInGroup(SessionInfo session)
@@ -132,7 +132,13 @@ namespace Emby.Server.Implementations.Syncplay
         private bool HasAccessToItem(User user, Guid itemId)
         private bool HasAccessToItem(User user, Guid itemId)
         {
         {
             var item = _libraryManager.GetItemById(itemId);
             var item = _libraryManager.GetItemById(itemId);
-            var hasParentalRatingAccess = user.Policy.MaxParentalRating.HasValue ? item.InheritedParentalRatingValue <= user.Policy.MaxParentalRating : true;
+
+            // Check ParentalRating access
+            var hasParentalRatingAccess = true;
+            if (user.Policy.MaxParentalRating.HasValue)
+            {
+                hasParentalRatingAccess = item.InheritedParentalRatingValue <= user.Policy.MaxParentalRating;
+            }
 
 
             if (!user.Policy.EnableAllFolders && hasParentalRatingAccess)
             if (!user.Policy.EnableAllFolders && hasParentalRatingAccess)
             {
             {
@@ -140,7 +146,7 @@ namespace Emby.Server.Implementations.Syncplay
                     folder => folder.Id.ToString("N", CultureInfo.InvariantCulture)
                     folder => folder.Id.ToString("N", CultureInfo.InvariantCulture)
                 );
                 );
                 var intersect = collections.Intersect(user.Policy.EnabledFolders);
                 var intersect = collections.Intersect(user.Policy.EnabledFolders);
-                return intersect.Count() > 0;
+                return intersect.Any();
             }
             }
             else
             else
             {
             {
@@ -163,13 +169,13 @@ namespace Emby.Server.Implementations.Syncplay
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public void NewGroup(SessionInfo session)
+        public void NewGroup(SessionInfo session, CancellationToken cancellationToken)
         {
         {
             var user = _userManager.GetUserById(session.UserId);
             var user = _userManager.GetUserById(session.UserId);
 
 
             if (user.Policy.SyncplayAccess != SyncplayAccess.CreateAndJoinGroups)
             if (user.Policy.SyncplayAccess != SyncplayAccess.CreateAndJoinGroups)
             {
             {
-                _logger.LogWarning("Syncplaymanager NewGroup: {0} does not have permission to create groups.", session.Id);
+                _logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id);
 
 
                 var error = new GroupUpdate<string>()
                 var error = new GroupUpdate<string>()
                 {
                 {
@@ -183,24 +189,24 @@ namespace Emby.Server.Implementations.Syncplay
             {
             {
                 if (IsSessionInGroup(session))
                 if (IsSessionInGroup(session))
                 {
                 {
-                    LeaveGroup(session);
+                    LeaveGroup(session, cancellationToken);
                 }
                 }
 
 
-                var group = new SyncplayController(_logger, _sessionManager, this);
+                var group = new SyncplayController(_sessionManager, this);
                 _groups[group.GetGroupId().ToString()] = group;
                 _groups[group.GetGroupId().ToString()] = group;
 
 
-                group.InitGroup(session);
+                group.InitGroup(session, cancellationToken);
             }
             }
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public void JoinGroup(SessionInfo session, string groupId, JoinGroupRequest request)
+        public void JoinGroup(SessionInfo session, string groupId, JoinGroupRequest request, CancellationToken cancellationToken)
         {
         {
             var user = _userManager.GetUserById(session.UserId);
             var user = _userManager.GetUserById(session.UserId);
 
 
             if (user.Policy.SyncplayAccess == SyncplayAccess.None)
             if (user.Policy.SyncplayAccess == SyncplayAccess.None)
             {
             {
-                _logger.LogWarning("Syncplaymanager JoinGroup: {0} does not have access to Syncplay.", session.Id);
+                _logger.LogWarning("JoinGroup: {0} does not have access to Syncplay.", session.Id);
 
 
                 var error = new GroupUpdate<string>()
                 var error = new GroupUpdate<string>()
                 {
                 {
@@ -217,11 +223,11 @@ namespace Emby.Server.Implementations.Syncplay
 
 
                 if (group == null)
                 if (group == null)
                 {
                 {
-                    _logger.LogWarning("Syncplaymanager JoinGroup: {0} tried to join group {0} that does not exist.", session.Id, groupId);
+                    _logger.LogWarning("JoinGroup: {0} tried to join group {0} that does not exist.", session.Id, groupId);
 
 
                     var error = new GroupUpdate<string>()
                     var error = new GroupUpdate<string>()
                     {
                     {
-                        Type = GroupUpdateType.GroupNotJoined
+                        Type = GroupUpdateType.GroupDoesNotExist
                     };
                     };
                     _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
                     _sessionManager.SendSyncplayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
                     return;
                     return;
@@ -229,7 +235,7 @@ namespace Emby.Server.Implementations.Syncplay
 
 
                 if (!HasAccessToItem(user, group.GetPlayingItemId()))
                 if (!HasAccessToItem(user, group.GetPlayingItemId()))
                 {
                 {
-                    _logger.LogWarning("Syncplaymanager JoinGroup: {0} does not have access to {1}.", session.Id, group.GetPlayingItemId());
+                    _logger.LogWarning("JoinGroup: {0} does not have access to {1}.", session.Id, group.GetPlayingItemId());
 
 
                     var error = new GroupUpdate<string>()
                     var error = new GroupUpdate<string>()
                     {
                     {
@@ -243,15 +249,15 @@ namespace Emby.Server.Implementations.Syncplay
                 if (IsSessionInGroup(session))
                 if (IsSessionInGroup(session))
                 {
                 {
                     if (GetSessionGroup(session).Equals(groupId)) return;
                     if (GetSessionGroup(session).Equals(groupId)) return;
-                    LeaveGroup(session);
+                    LeaveGroup(session, cancellationToken);
                 }
                 }
 
 
-                group.SessionJoin(session, request);
+                group.SessionJoin(session, request, cancellationToken);
             }
             }
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public void LeaveGroup(SessionInfo session)
+        public void LeaveGroup(SessionInfo session, CancellationToken cancellationToken)
         {
         {
             // TODO: determine what happens to users that are in a group and get their permissions revoked
             // TODO: determine what happens to users that are in a group and get their permissions revoked
             lock (_groupsLock)
             lock (_groupsLock)
@@ -261,7 +267,7 @@ namespace Emby.Server.Implementations.Syncplay
 
 
                 if (group == null)
                 if (group == null)
                 {
                 {
-                    _logger.LogWarning("Syncplaymanager LeaveGroup: {0} does not belong to any group.", session.Id);
+                    _logger.LogWarning("LeaveGroup: {0} does not belong to any group.", session.Id);
 
 
                     var error = new GroupUpdate<string>()
                     var error = new GroupUpdate<string>()
                     {
                     {
@@ -271,17 +277,18 @@ namespace Emby.Server.Implementations.Syncplay
                     return;
                     return;
                 }
                 }
 
 
-                group.SessionLeave(session);
+                group.SessionLeave(session, cancellationToken);
 
 
                 if (group.IsGroupEmpty())
                 if (group.IsGroupEmpty())
                 {
                 {
+                    _logger.LogInformation("LeaveGroup: removing empty group {0}.", group.GetGroupId());
                     _groups.Remove(group.GetGroupId().ToString(), out _);
                     _groups.Remove(group.GetGroupId().ToString(), out _);
                 }
                 }
             }
             }
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public List<GroupInfoView> ListGroups(SessionInfo session)
+        public List<GroupInfoView> ListGroups(SessionInfo session, Guid filterItemId)
         {
         {
             var user = _userManager.GetUserById(session.UserId);
             var user = _userManager.GetUserById(session.UserId);
 
 
@@ -290,11 +297,11 @@ namespace Emby.Server.Implementations.Syncplay
                 return new List<GroupInfoView>();
                 return new List<GroupInfoView>();
             }
             }
 
 
-            // Filter by playing item if the user is viewing something already
-            if (session.NowPlayingItem != null)
+            // Filter by item if requested
+            if (!filterItemId.Equals(Guid.Empty))
             {
             {
                 return _groups.Values.Where(
                 return _groups.Values.Where(
-                    group => group.GetPlayingItemId().Equals(session.FullNowPlayingItem.Id) && HasAccessToItem(user, group.GetPlayingItemId())
+                    group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId())
                 ).Select(
                 ).Select(
                     group => group.GetInfo()
                     group => group.GetInfo()
                 ).ToList();
                 ).ToList();
@@ -311,13 +318,13 @@ namespace Emby.Server.Implementations.Syncplay
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public void HandleRequest(SessionInfo session, PlaybackRequest request)
+        public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
         {
         {
             var user = _userManager.GetUserById(session.UserId);
             var user = _userManager.GetUserById(session.UserId);
 
 
             if (user.Policy.SyncplayAccess == SyncplayAccess.None)
             if (user.Policy.SyncplayAccess == SyncplayAccess.None)
             {
             {
-                _logger.LogWarning("Syncplaymanager HandleRequest: {0} does not have access to Syncplay.", session.Id);
+                _logger.LogWarning("HandleRequest: {0} does not have access to Syncplay.", session.Id);
 
 
                 var error = new GroupUpdate<string>()
                 var error = new GroupUpdate<string>()
                 {
                 {
@@ -334,7 +341,7 @@ namespace Emby.Server.Implementations.Syncplay
 
 
                 if (group == null)
                 if (group == null)
                 {
                 {
-                    _logger.LogWarning("Syncplaymanager HandleRequest: {0} does not belong to any group.", session.Id);
+                    _logger.LogWarning("HandleRequest: {0} does not belong to any group.", session.Id);
 
 
                     var error = new GroupUpdate<string>()
                     var error = new GroupUpdate<string>()
                     {
                     {
@@ -344,7 +351,7 @@ namespace Emby.Server.Implementations.Syncplay
                     return;
                     return;
                 }
                 }
 
 
-                group.HandleRequest(session, request);
+                group.HandleRequest(session, request, cancellationToken);
             }
             }
         }
         }
 
 

+ 55 - 30
MediaBrowser.Api/Syncplay/SyncplayService.cs

@@ -1,3 +1,4 @@
+using System.Threading;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
@@ -48,12 +49,19 @@ namespace MediaBrowser.Api.Syncplay
         public string SessionId { get; set; }
         public string SessionId { get; set; }
     }
     }
 
 
-    [Route("/Syncplay/{SessionId}/ListGroups", "POST", Summary = "List Syncplay groups playing same item")]
+    [Route("/Syncplay/{SessionId}/ListGroups", "POST", Summary = "List Syncplay groups")]
     [Authenticated]
     [Authenticated]
     public class SyncplayListGroups : IReturnVoid
     public class SyncplayListGroups : IReturnVoid
     {
     {
         [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
         [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
         public string SessionId { get; set; }
         public string SessionId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the filter item id.
+        /// </summary>
+        /// <value>The filter item id.</value>
+        [ApiMember(Name = "FilterItemId", Description = "Filter by item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string FilterItemId { get; set; }
     }
     }
 
 
     [Route("/Syncplay/{SessionId}/PlayRequest", "POST", Summary = "Request play in Syncplay group")]
     [Route("/Syncplay/{SessionId}/PlayRequest", "POST", Summary = "Request play in Syncplay group")]
@@ -104,8 +112,8 @@ namespace MediaBrowser.Api.Syncplay
         /// Gets or sets whether this is a buffering or a buffering-done request.
         /// Gets or sets whether this is a buffering or a buffering-done request.
         /// </summary>
         /// </summary>
         /// <value><c>true</c> if buffering is complete; <c>false</c> otherwise.</value>
         /// <value><c>true</c> if buffering is complete; <c>false</c> otherwise.</value>
-        [ApiMember(Name = "Resume", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")]
-        public bool Resume { get; set; }
+        [ApiMember(Name = "BufferingDone", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")]
+        public bool BufferingDone { get; set; }
     }
     }
 
 
     [Route("/Syncplay/{SessionId}/UpdatePing", "POST", Summary = "Update session ping")]
     [Route("/Syncplay/{SessionId}/UpdatePing", "POST", Summary = "Update session ping")]
@@ -124,11 +132,6 @@ namespace MediaBrowser.Api.Syncplay
     /// </summary>
     /// </summary>
     public class SyncplayService : BaseApiService
     public class SyncplayService : BaseApiService
     {
     {
-        /// <summary>
-        /// The session manager.
-        /// </summary>
-        private readonly ISessionManager _sessionManager;
-
         /// <summary>
         /// <summary>
         /// The session context.
         /// The session context.
         /// </summary>
         /// </summary>
@@ -143,12 +146,10 @@ namespace MediaBrowser.Api.Syncplay
             ILogger<SyncplayService> logger,
             ILogger<SyncplayService> logger,
             IServerConfigurationManager serverConfigurationManager,
             IServerConfigurationManager serverConfigurationManager,
             IHttpResultFactory httpResultFactory,
             IHttpResultFactory httpResultFactory,
-            ISessionManager sessionManager,
             ISessionContext sessionContext,
             ISessionContext sessionContext,
             ISyncplayManager syncplayManager)
             ISyncplayManager syncplayManager)
             : base(logger, serverConfigurationManager, httpResultFactory)
             : base(logger, serverConfigurationManager, httpResultFactory)
         {
         {
-            _sessionManager = sessionManager;
             _sessionContext = sessionContext;
             _sessionContext = sessionContext;
             _syncplayManager = syncplayManager;
             _syncplayManager = syncplayManager;
         }
         }
@@ -160,7 +161,7 @@ namespace MediaBrowser.Api.Syncplay
         public void Post(SyncplayNewGroup request)
         public void Post(SyncplayNewGroup request)
         {
         {
             var currentSession = GetSession(_sessionContext);
             var currentSession = GetSession(_sessionContext);
-            _syncplayManager.NewGroup(currentSession);
+            _syncplayManager.NewGroup(currentSession, CancellationToken.None);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -174,19 +175,27 @@ namespace MediaBrowser.Api.Syncplay
             {
             {
                 GroupId = Guid.Parse(request.GroupId)
                 GroupId = Guid.Parse(request.GroupId)
             };
             };
-            try
-            {
-                joinRequest.PlayingItemId = Guid.Parse(request.PlayingItemId);
-            }
-            catch (ArgumentNullException)
-            {
-                // Do nothing
-            }
-            catch (FormatException)
+
+            // Both null and empty strings mean that client isn't playing anything
+            if (!String.IsNullOrEmpty(request.PlayingItemId))
             {
             {
-                // Do nothing
+                try
+                {
+                    joinRequest.PlayingItemId = Guid.Parse(request.PlayingItemId);
+                }
+                catch (ArgumentNullException)
+                {
+                    // Should never happen, but just in case
+                    Logger.LogError("JoinGroup: null value for PlayingItemId. Ignoring request.");
+                    return;
+                }
+                catch (FormatException)
+                {
+                    Logger.LogError("JoinGroup: {0} is not a valid format for PlayingItemId. Ignoring request.", request.PlayingItemId);
+                    return;
+                }
             }
             }
-            _syncplayManager.JoinGroup(currentSession, request.GroupId, joinRequest);
+            _syncplayManager.JoinGroup(currentSession, request.GroupId, joinRequest, CancellationToken.None);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -196,7 +205,7 @@ namespace MediaBrowser.Api.Syncplay
         public void Post(SyncplayLeaveGroup request)
         public void Post(SyncplayLeaveGroup request)
         {
         {
             var currentSession = GetSession(_sessionContext);
             var currentSession = GetSession(_sessionContext);
-            _syncplayManager.LeaveGroup(currentSession);
+            _syncplayManager.LeaveGroup(currentSession, CancellationToken.None);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -207,7 +216,23 @@ namespace MediaBrowser.Api.Syncplay
         public List<GroupInfoView> Post(SyncplayListGroups request)
         public List<GroupInfoView> Post(SyncplayListGroups request)
         {
         {
             var currentSession = GetSession(_sessionContext);
             var currentSession = GetSession(_sessionContext);
-            return _syncplayManager.ListGroups(currentSession);
+            var filterItemId = Guid.Empty;
+            if (!String.IsNullOrEmpty(request.FilterItemId))
+            {
+                try
+                {
+                    filterItemId = Guid.Parse(request.FilterItemId);
+                }
+                catch (ArgumentNullException)
+                {
+                    Logger.LogWarning("ListGroups: null value for FilterItemId. Ignoring filter.");
+                }
+                catch (FormatException)
+                {
+                    Logger.LogWarning("ListGroups: {0} is not a valid format for FilterItemId. Ignoring filter.", request.FilterItemId);
+                }
+            }
+            return _syncplayManager.ListGroups(currentSession, filterItemId);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -221,7 +246,7 @@ namespace MediaBrowser.Api.Syncplay
             {
             {
                 Type = PlaybackRequestType.Play
                 Type = PlaybackRequestType.Play
             };
             };
-            _syncplayManager.HandleRequest(currentSession, syncplayRequest);
+            _syncplayManager.HandleRequest(currentSession, syncplayRequest, CancellationToken.None);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -235,7 +260,7 @@ namespace MediaBrowser.Api.Syncplay
             {
             {
                 Type = PlaybackRequestType.Pause
                 Type = PlaybackRequestType.Pause
             };
             };
-            _syncplayManager.HandleRequest(currentSession, syncplayRequest);
+            _syncplayManager.HandleRequest(currentSession, syncplayRequest, CancellationToken.None);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -250,7 +275,7 @@ namespace MediaBrowser.Api.Syncplay
                 Type = PlaybackRequestType.Seek,
                 Type = PlaybackRequestType.Seek,
                 PositionTicks = request.PositionTicks
                 PositionTicks = request.PositionTicks
             };
             };
-            _syncplayManager.HandleRequest(currentSession, syncplayRequest);
+            _syncplayManager.HandleRequest(currentSession, syncplayRequest, CancellationToken.None);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -262,11 +287,11 @@ namespace MediaBrowser.Api.Syncplay
             var currentSession = GetSession(_sessionContext);
             var currentSession = GetSession(_sessionContext);
             var syncplayRequest = new PlaybackRequest()
             var syncplayRequest = new PlaybackRequest()
             {
             {
-                Type = request.Resume ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering,
+                Type = request.BufferingDone ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering,
                 When = DateTime.Parse(request.When),
                 When = DateTime.Parse(request.When),
                 PositionTicks = request.PositionTicks
                 PositionTicks = request.PositionTicks
             };
             };
-            _syncplayManager.HandleRequest(currentSession, syncplayRequest);
+            _syncplayManager.HandleRequest(currentSession, syncplayRequest, CancellationToken.None);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -281,7 +306,7 @@ namespace MediaBrowser.Api.Syncplay
                 Type = PlaybackRequestType.UpdatePing,
                 Type = PlaybackRequestType.UpdatePing,
                 Ping = Convert.ToInt64(request.Ping)
                 Ping = Convert.ToInt64(request.Ping)
             };
             };
-            _syncplayManager.HandleRequest(currentSession, syncplayRequest);
+            _syncplayManager.HandleRequest(currentSession, syncplayRequest, CancellationToken.None);
         }
         }
     }
     }
 }
 }

+ 1 - 12
MediaBrowser.Api/Syncplay/TimeSyncService.cs

@@ -1,7 +1,6 @@
 using System;
 using System;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Services;
 using MediaBrowser.Model.Services;
 using MediaBrowser.Model.Syncplay;
 using MediaBrowser.Model.Syncplay;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
@@ -19,16 +18,6 @@ namespace MediaBrowser.Api.Syncplay
     /// </summary>
     /// </summary>
     public class TimeSyncService : BaseApiService
     public class TimeSyncService : BaseApiService
     {
     {
-        /// <summary>
-        /// The session manager.
-        /// </summary>
-        private readonly ISessionManager _sessionManager;
-
-        /// <summary>
-        /// The session context.
-        /// </summary>
-        private readonly ISessionContext _sessionContext;
-
         public TimeSyncService(
         public TimeSyncService(
             ILogger<TimeSyncService> logger,
             ILogger<TimeSyncService> logger,
             IServerConfigurationManager serverConfigurationManager,
             IServerConfigurationManager serverConfigurationManager,
@@ -55,7 +44,7 @@ namespace MediaBrowser.Api.Syncplay
             var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o");
             var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o");
             response.ResponseTransmissionTime = responseTransmissionTime;
             response.ResponseTransmissionTime = responseTransmissionTime;
 
 
-            // Implementing NTP on such a high level results in this useless 
+            // Implementing NTP on such a high level results in this useless
             // information being sent. On the other hand it enables future additions.
             // information being sent. On the other hand it enables future additions.
             return response;
             return response;
         }
         }

+ 5 - 3
MediaBrowser.Controller/Syncplay/GroupInfo.cs

@@ -1,5 +1,4 @@
 using System;
 using System;
-using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Session;
@@ -9,6 +8,9 @@ namespace MediaBrowser.Controller.Syncplay
     /// <summary>
     /// <summary>
     /// Class GroupInfo.
     /// Class GroupInfo.
     /// </summary>
     /// </summary>
+    /// <remarks>
+    /// Class is not thread-safe, external locking is required when accessing methods.
+    /// </remarks>
     public class GroupInfo
     public class GroupInfo
     {
     {
         /// <summary>
         /// <summary>
@@ -49,8 +51,8 @@ namespace MediaBrowser.Controller.Syncplay
         /// Gets the participants.
         /// Gets the participants.
         /// </summary>
         /// </summary>
         /// <value>The participants, or members of the group.</value>
         /// <value>The participants, or members of the group.</value>
-        public readonly ConcurrentDictionary<string, GroupMember> Participants =
-            new ConcurrentDictionary<string, GroupMember>(StringComparer.OrdinalIgnoreCase);
+        public readonly Dictionary<string, GroupMember> Participants =
+            new Dictionary<string, GroupMember>(StringComparer.OrdinalIgnoreCase);
 
 
         /// <summary>
         /// <summary>
         /// Checks if a session is in this group.
         /// Checks if a session is in this group.

+ 9 - 4
MediaBrowser.Controller/Syncplay/ISyncplayController.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using System.Threading;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Syncplay;
 using MediaBrowser.Model.Syncplay;
 
 
@@ -31,27 +32,31 @@ namespace MediaBrowser.Controller.Syncplay
         /// Initializes the group with the session's info.
         /// Initializes the group with the session's info.
         /// </summary>
         /// </summary>
         /// <param name="session">The session.</param>
         /// <param name="session">The session.</param>
-        void InitGroup(SessionInfo session);
+        /// <param name="cancellationToken">The cancellation token.</param>
+        void InitGroup(SessionInfo session, CancellationToken cancellationToken);
 
 
         /// <summary>
         /// <summary>
         /// Adds the session to the group.
         /// Adds the session to the group.
         /// </summary>
         /// </summary>
         /// <param name="session">The session.</param>
         /// <param name="session">The session.</param>
         /// <param name="request">The request.</param>
         /// <param name="request">The request.</param>
-        void SessionJoin(SessionInfo session, JoinGroupRequest request);
+        /// <param name="cancellationToken">The cancellation token.</param>
+        void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken);
 
 
         /// <summary>
         /// <summary>
         /// Removes the session from the group.
         /// Removes the session from the group.
         /// </summary>
         /// </summary>
         /// <param name="session">The session.</param>
         /// <param name="session">The session.</param>
-        void SessionLeave(SessionInfo session);
+        /// <param name="cancellationToken">The cancellation token.</param>
+        void SessionLeave(SessionInfo session, CancellationToken cancellationToken);
 
 
         /// <summary>
         /// <summary>
         /// Handles the requested action by the session.
         /// Handles the requested action by the session.
         /// </summary>
         /// </summary>
         /// <param name="session">The session.</param>
         /// <param name="session">The session.</param>
         /// <param name="request">The requested action.</param>
         /// <param name="request">The requested action.</param>
-        void HandleRequest(SessionInfo session, PlaybackRequest request);
+        /// <param name="cancellationToken">The cancellation token.</param>
+        void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken);
 
 
         /// <summary>
         /// <summary>
         /// Gets the info about the group for the clients.
         /// Gets the info about the group for the clients.

+ 11 - 5
MediaBrowser.Controller/Syncplay/ISyncplayManager.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Threading;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Syncplay;
 using MediaBrowser.Model.Syncplay;
 
 
@@ -14,7 +15,8 @@ namespace MediaBrowser.Controller.Syncplay
         /// Creates a new group.
         /// Creates a new group.
         /// </summary>
         /// </summary>
         /// <param name="session">The session that's creating the group.</param>
         /// <param name="session">The session that's creating the group.</param>
-        void NewGroup(SessionInfo session);
+        /// <param name="cancellationToken">The cancellation token.</param>
+        void NewGroup(SessionInfo session, CancellationToken cancellationToken);
 
 
         /// <summary>
         /// <summary>
         /// Adds the session to a group.
         /// Adds the session to a group.
@@ -22,27 +24,31 @@ namespace MediaBrowser.Controller.Syncplay
         /// <param name="session">The session.</param>
         /// <param name="session">The session.</param>
         /// <param name="groupId">The group id.</param>
         /// <param name="groupId">The group id.</param>
         /// <param name="request">The request.</param>
         /// <param name="request">The request.</param>
-        void JoinGroup(SessionInfo session, string groupId, JoinGroupRequest request);
+        /// <param name="cancellationToken">The cancellation token.</param>
+        void JoinGroup(SessionInfo session, string groupId, JoinGroupRequest request, CancellationToken cancellationToken);
 
 
         /// <summary>
         /// <summary>
         /// Removes the session from a group.
         /// Removes the session from a group.
         /// </summary>
         /// </summary>
         /// <param name="session">The session.</param>
         /// <param name="session">The session.</param>
-        void LeaveGroup(SessionInfo session);
+        /// <param name="cancellationToken">The cancellation token.</param>
+        void LeaveGroup(SessionInfo session, CancellationToken cancellationToken);
 
 
         /// <summary>
         /// <summary>
         /// Gets list of available groups for a session.
         /// Gets list of available groups for a session.
         /// </summary>
         /// </summary>
         /// <param name="session">The session.</param>
         /// <param name="session">The session.</param>
+        /// <param name="filterItemId">The item id to filter by.</param>
         /// <value>The list of available groups.</value>
         /// <value>The list of available groups.</value>
-        List<GroupInfoView> ListGroups(SessionInfo session);
+        List<GroupInfoView> ListGroups(SessionInfo session, Guid filterItemId);
 
 
         /// <summary>
         /// <summary>
         /// Handle a request by a session in a group.
         /// Handle a request by a session in a group.
         /// </summary>
         /// </summary>
         /// <param name="session">The session.</param>
         /// <param name="session">The session.</param>
         /// <param name="request">The request.</param>
         /// <param name="request">The request.</param>
-        void HandleRequest(SessionInfo session, PlaybackRequest request);
+        /// <param name="cancellationToken">The cancellation token.</param>
+        void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken);
 
 
         /// <summary>
         /// <summary>
         /// Maps a session to a group.
         /// Maps a session to a group.

+ 3 - 3
MediaBrowser.Model/Syncplay/GroupUpdateType.cs

@@ -30,13 +30,13 @@ namespace MediaBrowser.Model.Syncplay
         /// </summary>
         /// </summary>
         PrepareSession,
         PrepareSession,
         /// <summary>
         /// <summary>
-        /// The not-in-group error. Tells a user that it doesn't belong to a group.
+        /// The not-in-group error. Tells a user that they don't belong to a group.
         /// </summary>
         /// </summary>
         NotInGroup,
         NotInGroup,
         /// <summary>
         /// <summary>
-        /// The group-not-joined error. Sent when a request to join a group fails.
+        /// The group-does-not-exist error. Sent when trying to join a non-existing group.
         /// </summary>
         /// </summary>
-        GroupNotJoined,
+        GroupDoesNotExist,
         /// <summary>
         /// <summary>
         /// The create-group-denied error. Sent when a user tries to create a group without required permissions.
         /// The create-group-denied error. Sent when a user tries to create a group without required permissions.
         /// </summary>
         /// </summary>

+ 1 - 1
MediaBrowser.Model/Syncplay/PlaybackRequest.cs

@@ -11,7 +11,7 @@ namespace MediaBrowser.Model.Syncplay
         /// Gets or sets the request type.
         /// Gets or sets the request type.
         /// </summary>
         /// </summary>
         /// <value>The request type.</value>
         /// <value>The request type.</value>
-        public PlaybackRequestType Type;
+        public PlaybackRequestType Type { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets when the request has been made by the client.
         /// Gets or sets when the request has been made by the client.