2
0
Эх сурвалжийг харах

Implement syncplay backend

gion 5 жил өмнө
parent
commit
10c2c62f07

+ 4 - 0
Emby.Server.Implementations/ApplicationHost.cs

@@ -47,6 +47,7 @@ using Emby.Server.Implementations.Session;
 using Emby.Server.Implementations.SocketSharp;
 using Emby.Server.Implementations.SocketSharp;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.Updates;
 using Emby.Server.Implementations.Updates;
+using Emby.Server.Implementations.Syncplay;
 using MediaBrowser.Api;
 using MediaBrowser.Api;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
@@ -80,6 +81,7 @@ using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Controller.TV;
 using MediaBrowser.Controller.TV;
+using MediaBrowser.Controller.Syncplay;
 using MediaBrowser.LocalMetadata.Savers;
 using MediaBrowser.LocalMetadata.Savers;
 using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.Activity;
@@ -643,6 +645,8 @@ namespace Emby.Server.Implementations
 
 
             serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
             serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
 
 
+            serviceCollection.AddSingleton<ISyncplayManager, SyncplayManager>();
+
             serviceCollection.AddSingleton<LiveTvDtoService>();
             serviceCollection.AddSingleton<LiveTvDtoService>();
             serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
             serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
 
 

+ 17 - 0
Emby.Server.Implementations/Session/SessionManager.cs

@@ -25,6 +25,7 @@ using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Session;
+using MediaBrowser.Model.Syncplay;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.Session
 namespace Emby.Server.Implementations.Session
@@ -1154,6 +1155,22 @@ namespace Emby.Server.Implementations.Session
             await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
             await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
         }
         }
 
 
+        /// <inheritdoc />
+        public async Task SendSyncplayCommand(string sessionId, SyncplayCommand command, CancellationToken cancellationToken)
+        {
+            CheckDisposed();
+            var session = GetSessionToRemoteControl(sessionId);
+            await SendMessageToSession(session, "SyncplayCommand", command, cancellationToken).ConfigureAwait(false);
+        }
+
+        /// <inheritdoc />
+        public async Task SendSyncplayGroupUpdate<T>(string sessionId, SyncplayGroupUpdate<T> command, CancellationToken cancellationToken)
+        {
+            CheckDisposed();
+            var session = GetSessionToRemoteControl(sessionId);
+            await SendMessageToSession(session, "SyncplayGroupUpdate", command, cancellationToken).ConfigureAwait(false);
+        }
+
         private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
         private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
         {
         {
             var item = _libraryManager.GetItemById(id);
             var item = _libraryManager.GetItemById(id);

+ 418 - 0
Emby.Server.Implementations/Syncplay/SyncplayController.cs

@@ -0,0 +1,418 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.Syncplay;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.Syncplay;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.Syncplay
+{
+    /// <summary>
+    /// Class SyncplayController.
+    /// </summary>
+    public class SyncplayController : ISyncplayController, IDisposable
+    {
+        private enum BroadcastType
+        {
+            AllGroup = 0,
+            SingleUser = 1,
+            AllExceptUser = 2,
+            AllReady = 3
+        }
+
+        /// <summary>
+        /// The logger.
+        /// </summary>
+        private readonly ILogger _logger;
+
+        /// <summary>
+        /// The session manager.
+        /// </summary>
+        private readonly ISessionManager _sessionManager;
+
+        /// <summary>
+        /// The syncplay manager.
+        /// </summary>
+        private readonly ISyncplayManager _syncplayManager;
+
+        /// <summary>
+        /// The group to manage.
+        /// </summary>
+        private readonly GroupInfo _group = new GroupInfo();
+
+        /// <inheritdoc />
+        public Guid GetGroupId() => _group.GroupId;
+
+        /// <inheritdoc />
+        public Guid GetPlayingItemId() => _group.PlayingItem.Id;
+
+        /// <inheritdoc />
+        public bool IsGroupEmpty() => _group.IsEmpty();
+
+        private bool _disposed = false;
+
+        public SyncplayController(
+            ILogger logger,
+            ISessionManager sessionManager,
+            ISyncplayManager syncplayManager)
+        {
+            _logger = logger;
+            _sessionManager = sessionManager;
+            _syncplayManager = syncplayManager;
+        }
+
+        /// <inheritdoc />
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        /// <summary>
+        /// Releases unmanaged and optionally managed resources.
+        /// </summary>
+        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        protected virtual void Dispose(bool disposing)
+        {
+            if (_disposed)
+            {
+                return;
+            }
+
+            _disposed = true;
+        }
+
+        // TODO: use this somewhere
+        private void CheckDisposed()
+        {
+            if (_disposed)
+            {
+                throw new ObjectDisposedException(GetType().Name);
+            }
+        }
+
+        private SessionInfo[] FilterUsers(SessionInfo from, BroadcastType type)
+        {
+            if (type == BroadcastType.SingleUser)
+            {
+                return new SessionInfo[] { from };
+            }
+            else if (type == BroadcastType.AllGroup)
+            {
+                return _group.Partecipants.Values.Select(
+                    user => user.Session
+                ).ToArray();
+            }
+            else if (type == BroadcastType.AllExceptUser)
+            {
+                return _group.Partecipants.Values.Select(
+                    user => user.Session
+                ).Where(
+                    user => !user.Id.Equals(from.Id)
+                ).ToArray();
+            }
+            else if (type == BroadcastType.AllReady)
+            {
+                return _group.Partecipants.Values.Where(
+                    user => !user.IsBuffering
+                ).Select(
+                    user => user.Session
+                ).ToArray();
+            }
+            else
+            {
+                return new SessionInfo[] {};
+            }
+        }
+
+        private Task SendGroupUpdate<T>(SessionInfo from, BroadcastType type, SyncplayGroupUpdate<T> message)
+        {
+            IEnumerable<Task> GetTasks()
+            {
+                SessionInfo[] users = FilterUsers(from, type);
+                foreach (var user in users)
+                {
+                    yield return _sessionManager.SendSyncplayGroupUpdate(user.Id.ToString(), message, CancellationToken.None);
+                }
+            }
+
+            return Task.WhenAll(GetTasks());
+        }
+
+        private Task SendCommand(SessionInfo from, BroadcastType type, SyncplayCommand message)
+        {
+            IEnumerable<Task> GetTasks()
+            {
+                SessionInfo[] users = FilterUsers(from, type);
+                foreach (var user in users)
+                {
+                    yield return _sessionManager.SendSyncplayCommand(user.Id.ToString(), message, CancellationToken.None);
+                }
+            }
+
+            return Task.WhenAll(GetTasks());
+        }
+
+        private SyncplayCommand NewSyncplayCommand(SyncplayCommandType type) {
+            var command = new SyncplayCommand();
+            command.GroupId = _group.GroupId.ToString();
+            command.Command = type;
+            command.PositionTicks = _group.PositionTicks;
+            command.When = _group.LastActivity.ToUniversalTime().ToString("o");
+            return command;
+        }
+
+        private SyncplayGroupUpdate<T> NewSyncplayGroupUpdate<T>(SyncplayGroupUpdateType type, T data)
+        {
+            var command = new SyncplayGroupUpdate<T>();
+            command.GroupId = _group.GroupId.ToString();
+            command.Type = type;
+            command.Data = data;
+            return command;
+        }
+
+        /// <inheritdoc />
+        public void InitGroup(SessionInfo user)
+        {
+            _group.AddUser(user);
+            _syncplayManager.MapUserToGroup(user, this);
+
+            _group.PlayingItem = user.FullNowPlayingItem;
+            _group.IsPaused = true;
+            _group.PositionTicks = user.PlayState.PositionTicks ??= 0;
+            _group.LastActivity = DateTime.UtcNow;
+
+            var updateUser = NewSyncplayGroupUpdate(SyncplayGroupUpdateType.GroupJoined, DateTime.UtcNow.ToUniversalTime().ToString("o"));
+            SendGroupUpdate(user, BroadcastType.SingleUser, updateUser);
+            var pauseCommand = NewSyncplayCommand(SyncplayCommandType.Pause);
+            SendCommand(user, BroadcastType.SingleUser, pauseCommand);
+        }
+
+        /// <inheritdoc />
+        public void UserJoin(SessionInfo user)
+        {
+            if (user.NowPlayingItem != null && user.NowPlayingItem.Id.Equals(_group.PlayingItem.Id))
+            {
+                _group.AddUser(user);
+                _syncplayManager.MapUserToGroup(user, this);
+
+                var updateUser = NewSyncplayGroupUpdate(SyncplayGroupUpdateType.GroupJoined, _group.PositionTicks);
+                SendGroupUpdate(user, BroadcastType.SingleUser, updateUser);
+
+                var updateOthers = NewSyncplayGroupUpdate(SyncplayGroupUpdateType.UserJoined, user.UserName);
+                SendGroupUpdate(user, BroadcastType.AllExceptUser, updateOthers);
+
+                // Client join and play, syncing will happen client side
+                if (!_group.IsPaused)
+                {
+                    var playCommand = NewSyncplayCommand(SyncplayCommandType.Play);
+                    SendCommand(user, BroadcastType.SingleUser, playCommand);
+                }
+                else
+                {
+                    var pauseCommand = NewSyncplayCommand(SyncplayCommandType.Pause);
+                    SendCommand(user, BroadcastType.SingleUser, pauseCommand);
+                }
+            }
+            else
+            {
+                var playRequest = new PlayRequest();
+                playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id };
+                playRequest.StartPositionTicks = _group.PositionTicks;
+                var update = NewSyncplayGroupUpdate(SyncplayGroupUpdateType.PrepareSession, playRequest);
+                SendGroupUpdate(user, BroadcastType.SingleUser, update);
+            }
+        }
+
+        /// <inheritdoc />
+        public void UserLeave(SessionInfo user)
+        {
+            _group.RemoveUser(user);
+            _syncplayManager.UnmapUserFromGroup(user, this);
+
+            var updateUser = NewSyncplayGroupUpdate(SyncplayGroupUpdateType.GroupLeft, _group.PositionTicks);
+            SendGroupUpdate(user, BroadcastType.SingleUser, updateUser);
+
+            var updateOthers = NewSyncplayGroupUpdate(SyncplayGroupUpdateType.UserLeft, user.UserName);
+            SendGroupUpdate(user, BroadcastType.AllExceptUser, updateOthers);
+        }
+
+        /// <inheritdoc />
+        public void HandleRequest(SessionInfo user, SyncplayRequestInfo request)
+        {
+            if (request.Type.Equals(SyncplayRequestType.Play))
+            {
+                if (_group.IsPaused)
+                {
+                    var delay = _group.GetHighestPing() * 2;
+                    delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
+
+                    _group.IsPaused = false;
+                    _group.LastActivity = DateTime.UtcNow.AddMilliseconds(
+                        delay
+                    );
+
+                    var command = NewSyncplayCommand(SyncplayCommandType.Play);
+                    SendCommand(user, BroadcastType.AllGroup, command);
+                }
+                else
+                {
+                    // Client got lost
+                    var command = NewSyncplayCommand(SyncplayCommandType.Play);
+                    SendCommand(user, BroadcastType.SingleUser, command);
+                }
+            }
+            else if (request.Type.Equals(SyncplayRequestType.Pause))
+            {
+                if (!_group.IsPaused)
+                {
+                    _group.IsPaused = true;
+                    var currentTime = DateTime.UtcNow;
+                    var elapsedTime = currentTime - _group.LastActivity;
+                    _group.LastActivity = currentTime;
+                    _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
+
+                    var command = NewSyncplayCommand(SyncplayCommandType.Pause);
+                    SendCommand(user, BroadcastType.AllGroup, command);
+                }
+                else
+                {
+                    var command = NewSyncplayCommand(SyncplayCommandType.Pause);
+                    SendCommand(user, BroadcastType.SingleUser, command);
+                }
+            }
+            else if (request.Type.Equals(SyncplayRequestType.Seek))
+            {
+                // 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;
+                }
+
+                _group.IsPaused = true;
+                _group.PositionTicks = ticks;
+                _group.LastActivity = DateTime.UtcNow;
+
+                var command = NewSyncplayCommand(SyncplayCommandType.Seek);
+                SendCommand(user, BroadcastType.AllGroup, command);
+            }
+            // TODO: client does not implement this yet
+            else if (request.Type.Equals(SyncplayRequestType.Buffering))
+            {
+                if (!_group.IsPaused)
+                {
+                    _group.IsPaused = true;
+                    var currentTime = DateTime.UtcNow;
+                    var elapsedTime = currentTime - _group.LastActivity;
+                    _group.LastActivity = currentTime;
+                    _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
+
+                    _group.SetBuffering(user, true);
+
+                    // Send pause command to all non-buffering users
+                    var command = NewSyncplayCommand(SyncplayCommandType.Pause);
+                    SendCommand(user, BroadcastType.AllReady, command);
+
+                    var updateOthers = NewSyncplayGroupUpdate(SyncplayGroupUpdateType.GroupWait, user.UserName);
+                    SendGroupUpdate(user, BroadcastType.AllExceptUser, updateOthers);
+                }
+                else
+                {
+                    var command = NewSyncplayCommand(SyncplayCommandType.Pause);
+                    SendCommand(user, BroadcastType.SingleUser, command);
+                }
+            }
+            // TODO: client does not implement this yet
+            else if (request.Type.Equals(SyncplayRequestType.BufferingComplete))
+            {
+                if (_group.IsPaused)
+                {
+                    _group.SetBuffering(user, false);
+
+                    if (_group.IsBuffering()) {
+                        // Others are buffering, tell this client to pause when ready
+                        var when = request.When ??= DateTime.UtcNow;
+                        var currentTime = DateTime.UtcNow;
+                        var elapsedTime = currentTime - when;
+                        var clientPosition = TimeSpan.FromTicks(request.PositionTicks ??= 0) + elapsedTime;
+                        var delay = _group.PositionTicks - clientPosition.Ticks;
+
+                        var command = NewSyncplayCommand(SyncplayCommandType.Pause);
+                        command.When = currentTime.AddMilliseconds(
+                            delay
+                        ).ToUniversalTime().ToString("o");
+                        SendCommand(user, BroadcastType.SingleUser, command);
+                    }
+                    else
+                    {
+                        // Let other clients resume as soon as the buffering client catches up
+                        var when = request.When ??= DateTime.UtcNow;
+                        var currentTime = DateTime.UtcNow;
+                        var elapsedTime = currentTime - when;
+                        var clientPosition = TimeSpan.FromTicks(request.PositionTicks ??= 0) + elapsedTime;
+                        var delay = _group.PositionTicks - clientPosition.Ticks;
+
+                        _group.IsPaused = false;
+
+                        if (delay > _group.GetHighestPing() * 2)
+                        {
+                            // Client that was buffering is recovering, notifying others to resume
+                            _group.LastActivity = currentTime.AddMilliseconds(
+                                delay
+                            );
+                            var command = NewSyncplayCommand(SyncplayCommandType.Play);
+                            SendCommand(user, BroadcastType.AllExceptUser, command);
+                        }
+                        else
+                        {
+                            // Client, that was buffering, resumed playback but did not update others in time
+                            delay = _group.GetHighestPing() * 2;
+                            delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
+
+                            _group.LastActivity = currentTime.AddMilliseconds(
+                                delay
+                            );
+
+                            var command = NewSyncplayCommand(SyncplayCommandType.Play);
+                            SendCommand(user, BroadcastType.AllGroup, command);
+                        }
+                    }                    
+                }
+                else
+                {
+                    // Make sure client has latest group state
+                    var command = NewSyncplayCommand(SyncplayCommandType.Play);
+                    SendCommand(user, BroadcastType.SingleUser, command);
+                }
+            }
+            else if (request.Type.Equals(SyncplayRequestType.KeepAlive))
+            {
+                _group.UpdatePing(user, request.Ping ??= _group.DefaulPing);
+
+                var keepAlive = new SyncplayGroupUpdate<string>();
+                keepAlive.GroupId = _group.GroupId.ToString();
+                keepAlive.Type = SyncplayGroupUpdateType.KeepAlive;
+                SendGroupUpdate(user, BroadcastType.SingleUser, keepAlive);
+            }
+        }
+
+        /// <inheritdoc />
+        public GroupInfoView GetInfo()
+        {
+            var info = new GroupInfoView();
+            info.GroupId = GetGroupId().ToString();
+            info.PlayingItemName = _group.PlayingItem.Name;
+            info.PlayingItemId = _group.PlayingItem.Id.ToString();
+            info.PositionTicks = _group.PositionTicks;
+            info.Partecipants = _group.Partecipants.Values.Select(user => user.Session.UserName).ToArray();
+            return info;
+        }
+    }
+}

+ 248 - 0
Emby.Server.Implementations/Syncplay/SyncplayManager.cs

@@ -0,0 +1,248 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using Microsoft.Extensions.Logging;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.Syncplay;
+using MediaBrowser.Model.Syncplay;
+
+namespace Emby.Server.Implementations.Syncplay
+{
+    /// <summary>
+    /// Class SyncplayManager.
+    /// </summary>
+    public class SyncplayManager : ISyncplayManager, IDisposable
+    {
+        /// <summary>
+        /// The logger.
+        /// </summary>
+        private readonly ILogger _logger;
+
+        /// <summary>
+        /// The session manager.
+        /// </summary>
+        private readonly ISessionManager _sessionManager;
+
+        /// <summary>
+        /// The map between users and groups.
+        /// </summary>
+        private readonly ConcurrentDictionary<string, ISyncplayController> _userToGroupMap =
+            new ConcurrentDictionary<string, ISyncplayController>(StringComparer.OrdinalIgnoreCase);
+
+        /// <summary>
+        /// The groups.
+        /// </summary>
+        private readonly ConcurrentDictionary<string, ISyncplayController> _groups =
+        new ConcurrentDictionary<string, ISyncplayController>(StringComparer.OrdinalIgnoreCase);
+
+        private bool _disposed = false;
+
+        public SyncplayManager(
+            ILogger<SyncplayManager> logger,
+            ISessionManager sessionManager)
+        {
+            _logger = logger;
+            _sessionManager = sessionManager;
+
+            _sessionManager.SessionEnded += _sessionManager_SessionEnded;
+            _sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped;
+        }
+
+        /// <summary>
+        /// Gets all groups.
+        /// </summary>
+        /// <value>All groups.</value>
+        public IEnumerable<ISyncplayController> Groups => _groups.Values;
+
+        /// <inheritdoc />
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        /// <summary>
+        /// Releases unmanaged and optionally managed resources.
+        /// </summary>
+        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        protected virtual void Dispose(bool disposing)
+        {
+            if (_disposed)
+            {
+                return;
+            }
+
+            _sessionManager.SessionEnded -= _sessionManager_SessionEnded;
+            _sessionManager.PlaybackStopped -= _sessionManager_PlaybackStopped;
+
+            _disposed = true;
+        }
+
+        private void CheckDisposed()
+        {
+            if (_disposed)
+            {
+                throw new ObjectDisposedException(GetType().Name);
+            }
+        }
+
+        void _sessionManager_SessionEnded(object sender, SessionEventArgs e)
+        {
+            LeaveGroup(e.SessionInfo);
+        }
+
+        void _sessionManager_PlaybackStopped(object sender, PlaybackStopEventArgs e)
+        {
+            LeaveGroup(e.Session);
+        }
+
+        private bool IsUserInGroup(SessionInfo user)
+        {
+            return _userToGroupMap.ContainsKey(user.Id);
+        }
+
+        private Guid? GetUserGroup(SessionInfo user)
+        {
+            ISyncplayController group;
+            _userToGroupMap.TryGetValue(user.Id, out group);
+            if (group != null)
+            {
+                return group.GetGroupId();
+            }
+            else
+            {
+                return null;
+            }
+        }
+
+        /// <inheritdoc />
+        public void NewGroup(SessionInfo user)
+        {
+            if (IsUserInGroup(user))
+            {
+                LeaveGroup(user);
+            }
+
+            var group = new SyncplayController(_logger, _sessionManager, this);
+            _groups[group.GetGroupId().ToString()] = group;
+
+            group.InitGroup(user);
+        }
+
+        /// <inheritdoc />
+        public void JoinGroup(SessionInfo user, string groupId)
+        {
+            if (IsUserInGroup(user))
+            {
+                if (GetUserGroup(user).Equals(groupId)) return;
+                LeaveGroup(user);
+            }
+
+            ISyncplayController group;
+            _groups.TryGetValue(groupId, out group);
+
+            if (group == null)
+            {
+                _logger.LogError("Syncplaymanager JoinGroup: " + groupId + " does not exist.");
+
+                var update = new SyncplayGroupUpdate<string>();
+                update.Type = SyncplayGroupUpdateType.NotInGroup;
+                _sessionManager.SendSyncplayGroupUpdate(user.Id.ToString(), update, CancellationToken.None);
+                return;
+            }
+            group.UserJoin(user);
+        }
+
+        /// <inheritdoc />
+        public void LeaveGroup(SessionInfo user)
+        {
+            ISyncplayController group;
+            _userToGroupMap.TryGetValue(user.Id, out group);
+
+            if (group == null)
+            {
+                _logger.LogWarning("Syncplaymanager HandleRequest: " + user.Id + " not in group.");
+
+                var update = new SyncplayGroupUpdate<string>();
+                update.Type = SyncplayGroupUpdateType.NotInGroup;
+                _sessionManager.SendSyncplayGroupUpdate(user.Id.ToString(), update, CancellationToken.None);
+                return;
+            }
+            group.UserLeave(user);
+
+            if (group.IsGroupEmpty())
+            {
+                _groups.Remove(group.GetGroupId().ToString(), out _);
+            }
+        }
+
+        /// <inheritdoc />
+        public List<GroupInfoView> ListGroups(SessionInfo user)
+        {
+            // Filter by playing item if the user is viewing something already
+            if (user.NowPlayingItem != null)
+            {
+                return _groups.Values.Where(
+                    group => group.GetPlayingItemId().Equals(user.FullNowPlayingItem.Id)
+                ).Select(
+                    group => group.GetInfo()
+                ).ToList();
+            }
+            // Otherwise show all available groups
+            else
+            {
+                return _groups.Values.Select(
+                    group => group.GetInfo()
+                ).ToList();
+            }
+        }
+
+        /// <inheritdoc />
+        public void HandleRequest(SessionInfo user, SyncplayRequestInfo request)
+        {
+            ISyncplayController group;
+            _userToGroupMap.TryGetValue(user.Id, out group);
+
+            if (group == null)
+            {
+                _logger.LogWarning("Syncplaymanager HandleRequest: " + user.Id + " not in group.");
+
+                var update = new SyncplayGroupUpdate<string>();
+                update.Type = SyncplayGroupUpdateType.NotInGroup;
+                _sessionManager.SendSyncplayGroupUpdate(user.Id.ToString(), update, CancellationToken.None);
+                return;
+            }
+            group.HandleRequest(user, request);
+        }
+        
+        /// <inheritdoc />
+        public void MapUserToGroup(SessionInfo user, ISyncplayController group)
+        {
+            if (IsUserInGroup(user))
+            {
+                throw new InvalidOperationException("User in other group already!");
+            }
+            _userToGroupMap[user.Id] = group;
+        }
+
+        /// <inheritdoc />
+        public void UnmapUserFromGroup(SessionInfo user, ISyncplayController group)
+        {
+            if (!IsUserInGroup(user))
+            {
+                throw new InvalidOperationException("User not in any group!");
+            }
+
+            ISyncplayController tempGroup;
+            _userToGroupMap.Remove(user.Id, out tempGroup);
+
+            if (!tempGroup.GetGroupId().Equals(group.GetGroupId()))
+            {
+                throw new InvalidOperationException("User was in wrong group!");
+            }
+        }
+    }
+}

+ 261 - 0
MediaBrowser.Api/Syncplay/SyncplayService.cs

@@ -0,0 +1,261 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.Syncplay;
+using MediaBrowser.Model.Services;
+using MediaBrowser.Model.Syncplay;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Api.Syncplay
+{
+    [Route("/Syncplay/{SessionId}/NewGroup", "POST", Summary = "Create a new Syncplay group")]
+    [Authenticated]
+    public class SyncplayNewGroup : IReturnVoid
+    {
+        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string SessionId { get; set; }
+    }
+
+    [Route("/Syncplay/{SessionId}/JoinGroup", "POST", Summary = "Join an existing Syncplay group")]
+    [Authenticated]
+    public class SyncplayJoinGroup : IReturnVoid
+    {
+        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string SessionId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the Group id.
+        /// </summary>
+        /// <value>The Group id to join.</value>
+        [ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string GroupId { get; set; }
+    }
+
+    [Route("/Syncplay/{SessionId}/LeaveGroup", "POST", Summary = "Leave joined Syncplay group")]
+    [Authenticated]
+    public class SyncplayLeaveGroup : IReturnVoid
+    {
+        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string SessionId { get; set; }
+    }
+
+    [Route("/Syncplay/{SessionId}/ListGroups", "POST", Summary = "List Syncplay groups playing same item")]
+    [Authenticated]
+    public class SyncplayListGroups : IReturnVoid
+    {
+        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string SessionId { get; set; }
+    }
+
+    [Route("/Syncplay/{SessionId}/PlayRequest", "POST", Summary = "Request play in Syncplay group")]
+    [Authenticated]
+    public class SyncplayPlayRequest : IReturnVoid
+    {
+        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string SessionId { get; set; }
+    }
+
+    [Route("/Syncplay/{SessionId}/PauseRequest", "POST", Summary = "Request pause in Syncplay group")]
+    [Authenticated]
+    public class SyncplayPauseRequest : IReturnVoid
+    {
+        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string SessionId { get; set; }
+    }
+
+    [Route("/Syncplay/{SessionId}/SeekRequest", "POST", Summary = "Request seek in Syncplay group")]
+    [Authenticated]
+    public class SyncplaySeekRequest : IReturnVoid
+    {
+        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string SessionId { get; set; }
+
+        [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")]
+        public long PositionTicks { get; set; }
+    }
+
+    [Route("/Syncplay/{SessionId}/BufferingRequest", "POST", Summary = "Request group wait in Syncplay group while buffering")]
+    [Authenticated]
+    public class SyncplayBufferingRequest : IReturnVoid
+    {
+        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string SessionId { get; set; }
+
+        [ApiMember(Name = "When", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
+        public string When { get; set; }
+
+        [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")]
+        public long PositionTicks { get; set; }
+
+        [ApiMember(Name = "Resume", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")]
+        public bool Resume { get; set; }
+    }
+
+    [Route("/Syncplay/{SessionId}/KeepAlive", "POST", Summary = "Keep session alive")]
+    [Authenticated]
+    public class SyncplayKeepAlive : IReturnVoid
+    {
+        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string SessionId { get; set; }
+
+        [ApiMember(Name = "Ping", IsRequired = true, DataType = "double", ParameterType = "query", Verb = "POST")]
+        public double Ping { get; set; }
+    }
+
+    [Route("/Syncplay/{SessionId}/GetUtcTime", "POST", Summary = "Get UtcTime")]
+    [Authenticated]
+    public class SyncplayGetUtcTime : IReturnVoid
+    {
+        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string SessionId { get; set; }
+    }
+
+    /// <summary>
+    /// Class SyncplayService.
+    /// </summary>
+    public class SyncplayService : BaseApiService
+    {
+        /// <summary>
+        /// The session manager.
+        /// </summary>
+        private readonly ISessionManager _sessionManager;
+
+        private readonly ISessionContext _sessionContext;
+
+        /// <summary>
+        /// The Syncplay manager.
+        /// </summary>
+        private readonly ISyncplayManager _syncplayManager;
+
+        public SyncplayService(
+            ILogger<SyncplayService> logger,
+            IServerConfigurationManager serverConfigurationManager,
+            IHttpResultFactory httpResultFactory,
+            ISessionManager sessionManager,
+            ISessionContext sessionContext,
+            ISyncplayManager syncplayManager)
+            : base(logger, serverConfigurationManager, httpResultFactory)
+        {
+            _sessionManager = sessionManager;
+            _sessionContext = sessionContext;
+            _syncplayManager = syncplayManager;
+        }
+
+        /// <summary>
+        /// Handles the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(SyncplayNewGroup request)
+        {
+            var currentSession = GetSession(_sessionContext);
+            _syncplayManager.NewGroup(currentSession);
+        }
+
+        /// <summary>
+        /// Handles the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(SyncplayJoinGroup request)
+        {
+            var currentSession = GetSession(_sessionContext);
+            _syncplayManager.JoinGroup(currentSession, request.GroupId);
+        }
+
+        /// <summary>
+        /// Handles the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(SyncplayLeaveGroup request)
+        {
+            var currentSession = GetSession(_sessionContext);
+            _syncplayManager.LeaveGroup(currentSession);
+        }
+
+        /// <summary>
+        /// Handles the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <value>The requested list of groups.</value>
+        public List<GroupInfoView> Post(SyncplayListGroups request)
+        {
+            var currentSession = GetSession(_sessionContext);
+            return _syncplayManager.ListGroups(currentSession);
+        }
+
+        /// <summary>
+        /// Handles the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(SyncplayPlayRequest request)
+        {
+            var currentSession = GetSession(_sessionContext);
+            var syncplayRequest = new SyncplayRequestInfo();
+            syncplayRequest.Type = SyncplayRequestType.Play;
+            _syncplayManager.HandleRequest(currentSession, syncplayRequest);
+        }
+
+        /// <summary>
+        /// Handles the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(SyncplayPauseRequest request)
+        {
+            var currentSession = GetSession(_sessionContext);
+            var syncplayRequest = new SyncplayRequestInfo();
+            syncplayRequest.Type = SyncplayRequestType.Pause;
+            _syncplayManager.HandleRequest(currentSession, syncplayRequest);
+        }
+
+        /// <summary>
+        /// Handles the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(SyncplaySeekRequest request)
+        {
+            var currentSession = GetSession(_sessionContext);
+            var syncplayRequest = new SyncplayRequestInfo();
+            syncplayRequest.Type = SyncplayRequestType.Seek;
+            syncplayRequest.PositionTicks = request.PositionTicks;
+            _syncplayManager.HandleRequest(currentSession, syncplayRequest);
+        }
+
+        /// <summary>
+        /// Handles the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(SyncplayBufferingRequest request)
+        {
+            var currentSession = GetSession(_sessionContext);
+            var syncplayRequest = new SyncplayRequestInfo();
+            syncplayRequest.Type = request.Resume ? SyncplayRequestType.BufferingComplete : SyncplayRequestType.Buffering;
+            syncplayRequest.When = DateTime.Parse(request.When);
+            syncplayRequest.PositionTicks = request.PositionTicks;
+            _syncplayManager.HandleRequest(currentSession, syncplayRequest);
+        }
+
+        /// <summary>
+        /// Handles the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        public void Post(SyncplayKeepAlive request)
+        {
+            var currentSession = GetSession(_sessionContext);
+            var syncplayRequest = new SyncplayRequestInfo();
+            syncplayRequest.Type = SyncplayRequestType.KeepAlive;
+            syncplayRequest.Ping = Convert.ToInt64(request.Ping);
+            _syncplayManager.HandleRequest(currentSession, syncplayRequest);
+        }
+
+        /// <summary>
+        /// Handles the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <value>The current UTC time.</value>
+        public string Post(SyncplayGetUtcTime request)
+        {
+            return DateTime.UtcNow.ToUniversalTime().ToString("o");
+        }
+    }
+}

+ 19 - 0
MediaBrowser.Controller/Session/ISessionManager.cs

@@ -9,6 +9,7 @@ using MediaBrowser.Controller.Security;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Session;
+using MediaBrowser.Model.Syncplay;
 
 
 namespace MediaBrowser.Controller.Session
 namespace MediaBrowser.Controller.Session
 {
 {
@@ -140,6 +141,24 @@ namespace MediaBrowser.Controller.Session
         /// <returns>Task.</returns>
         /// <returns>Task.</returns>
         Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken);
         Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken);
 
 
+        /// <summary>
+        /// Sends the SyncplayCommand.
+        /// </summary>
+        /// <param name="sessionId">The session id.</param>
+        /// <param name="command">The command.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task SendSyncplayCommand(string sessionId, SyncplayCommand command, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Sends the SyncplayGroupUpdate.
+        /// </summary>
+        /// <param name="sessionId">The session id.</param>
+        /// <param name="command">The group update.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task SendSyncplayGroupUpdate<T>(string sessionId, SyncplayGroupUpdate<T> command, CancellationToken cancellationToken);
+
         /// <summary>
         /// <summary>
         /// Sends the browse command.
         /// Sends the browse command.
         /// </summary>
         /// </summary>

+ 148 - 0
MediaBrowser.Controller/Syncplay/GroupInfo.cs

@@ -0,0 +1,148 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.Syncplay
+{
+    /// <summary>
+    /// Class GroupInfo.
+    /// </summary>
+    public class GroupInfo
+    {
+        /// <summary>
+        /// Default ping value used for users.
+        /// </summary>
+        public readonly long DefaulPing = 500;
+        /// <summary>
+        /// Gets or sets the group identifier.
+        /// </summary>
+        /// <value>The group identifier.</value>
+        public readonly Guid GroupId = Guid.NewGuid();
+
+        /// <summary>
+        /// Gets or sets the playing item.
+        /// </summary>
+        /// <value>The playing item.</value>
+        public BaseItem PlayingItem { get; set; }
+
+        /// <summary>
+        /// Gets or sets whether playback is paused.
+        /// </summary>
+        /// <value>Playback is paused.</value>
+        public bool IsPaused { get; set; }
+
+        /// <summary>
+        /// Gets or sets the position ticks.
+        /// </summary>
+        /// <value>The position ticks.</value>
+        public long PositionTicks { get; set; }
+
+        /// <summary>
+        /// Gets or sets the last activity.
+        /// </summary>
+        /// <value>The last activity.</value>
+        public DateTime LastActivity { get; set; }
+
+        /// <summary>
+        /// Gets the partecipants.
+        /// </summary>
+        /// <value>The partecipants.</value>
+        public readonly ConcurrentDictionary<string, GroupMember> Partecipants =
+        new ConcurrentDictionary<string, GroupMember>(StringComparer.OrdinalIgnoreCase);
+
+        /// <summary>
+        /// Checks if a user is in this group.
+        /// </summary>
+        /// <value><c>true</c> if the user is in this group; <c>false</c> otherwise.</value>
+        public bool ContainsUser(string sessionId)
+        {
+            return Partecipants.ContainsKey(sessionId);
+        }
+
+        /// <summary>
+        /// Adds the user to the group.
+        /// </summary>
+        /// <param name="user">The session.</param>
+        public void AddUser(SessionInfo user)
+        {
+            if (ContainsUser(user.Id.ToString())) return;
+            var member = new GroupMember();
+            member.Session = user;
+            member.Ping = DefaulPing;
+            member.IsBuffering = false;
+            Partecipants[user.Id.ToString()] = member;
+        }
+
+        /// <summary>
+        /// Removes the user from the group.
+        /// </summary>
+        /// <param name="user">The session.</param>
+
+        public void RemoveUser(SessionInfo user)
+        {
+            if (!ContainsUser(user.Id.ToString())) return;
+            GroupMember member;
+            Partecipants.Remove(user.Id.ToString(), out member);
+        }
+
+        /// <summary>
+        /// Updates the ping of a user.
+        /// </summary>
+        /// <param name="user">The session.</param>
+        /// <param name="ping">The ping.</param>
+        public void UpdatePing(SessionInfo user, long ping)
+        {
+            if (!ContainsUser(user.Id.ToString())) return;
+            Partecipants[user.Id.ToString()].Ping = ping;
+        }
+
+        /// <summary>
+        /// Gets the highest ping in the group.
+        /// </summary>
+        /// <value name="user">The highest ping in the group.</value>
+        public long GetHighestPing()
+        {
+            long max = Int64.MinValue;
+            foreach (var user in Partecipants.Values)
+            {
+                max = Math.Max(max, user.Ping);
+            }
+            return max;
+        }
+
+        /// <summary>
+        /// Sets the user's buffering state.
+        /// </summary>
+        /// <param name="user">The session.</param>
+        /// <param name="isBuffering">The state.</param>
+        public void SetBuffering(SessionInfo user, bool isBuffering)
+        {
+            if (!ContainsUser(user.Id.ToString())) return;
+            Partecipants[user.Id.ToString()].IsBuffering = isBuffering;
+        }
+
+        /// <summary>
+        /// Gets the group buffering state.
+        /// </summary>
+        /// <value><c>true</c> if there is a user buffering in the group; <c>false</c> otherwise.</value>
+        public bool IsBuffering()
+        {
+            foreach (var user in Partecipants.Values)
+            {
+                if (user.IsBuffering) return true;
+            }
+            return false;
+        }
+
+        /// <summary>
+        /// Checks if the group is empty.
+        /// </summary>
+        /// <value><c>true</c> if the group is empty; <c>false</c> otherwise.</value>
+        public bool IsEmpty()
+        {
+            return Partecipants.Count == 0;
+        }
+    }
+}

+ 28 - 0
MediaBrowser.Controller/Syncplay/GroupMember.cs

@@ -0,0 +1,28 @@
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.Syncplay
+{
+    /// <summary>
+    /// Class GroupMember.
+    /// </summary>
+    public class GroupMember
+    {
+        /// <summary>
+        /// Gets or sets whether this member is buffering.
+        /// </summary>
+        /// <value><c>true</c> if member is buffering; <c>false</c> otherwise.</value>
+        public bool IsBuffering { get; set; }
+
+        /// <summary>
+        /// Gets or sets the session.
+        /// </summary>
+        /// <value>The session.</value>
+        public SessionInfo Session { get; set; }
+
+        /// <summary>
+        /// Gets or sets the ping.
+        /// </summary>
+        /// <value>The ping.</value>
+        public long Ping { get; set; }
+    }
+}

+ 61 - 0
MediaBrowser.Controller/Syncplay/ISyncplayController.cs

@@ -0,0 +1,61 @@
+using System;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Syncplay;
+
+namespace MediaBrowser.Controller.Syncplay
+{
+    /// <summary>
+    /// Interface ISyncplayController.
+    /// </summary>
+    public interface ISyncplayController
+    {
+        /// <summary>
+        /// Gets the group id.
+        /// </summary>
+        /// <value>The group id.</value>
+        Guid GetGroupId();
+
+        /// <summary>
+        /// Gets the playing item id.
+        /// </summary>
+        /// <value>The playing item id.</value>
+        Guid GetPlayingItemId();
+
+        /// <summary>
+        /// Checks if the group is empty.
+        /// </summary>
+        /// <value>If the group is empty.</value>
+        bool IsGroupEmpty();
+
+        /// <summary>
+        /// Initializes the group with the user's info.
+        /// </summary>
+        /// <param name="user">The session.</param>
+        void InitGroup(SessionInfo user);
+
+        /// <summary>
+        /// Adds the user to the group.
+        /// </summary>
+        /// <param name="user">The session.</param>
+        void UserJoin(SessionInfo user);
+
+        /// <summary>
+        /// Removes the user from the group.
+        /// </summary>
+        /// <param name="user">The session.</param>
+        void UserLeave(SessionInfo user);
+
+        /// <summary>
+        /// Handles the requested action by the user.
+        /// </summary>
+        /// <param name="user">The session.</param>
+        /// <param name="request">The requested action.</param>
+        void HandleRequest(SessionInfo user, SyncplayRequestInfo request);
+
+        /// <summary>
+        /// Gets the info about the group for the clients.
+        /// </summary>
+        /// <value>The group info for the clients.</value>
+        GroupInfoView GetInfo();
+    }
+}

+ 62 - 0
MediaBrowser.Controller/Syncplay/ISyncplayManager.cs

@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Syncplay;
+
+namespace MediaBrowser.Controller.Syncplay
+{
+    /// <summary>
+    /// Interface ISyncplayManager.
+    /// </summary>
+    public interface ISyncplayManager
+    {
+        /// <summary>
+        /// Creates a new group.
+        /// </summary>
+        /// <param name="user">The user that's creating the group.</param>
+        void NewGroup(SessionInfo user);
+
+        /// <summary>
+        /// Adds the user to a group.
+        /// </summary>
+        /// <param name="user">The session.</param>
+        /// <param name="groupId">The group id.</param>
+        void JoinGroup(SessionInfo user, string groupId);
+
+        /// <summary>
+        /// Removes the user from a group.
+        /// </summary>
+        /// <param name="user">The session.</param>
+        void LeaveGroup(SessionInfo user);
+
+        /// <summary>
+        /// Gets list of available groups for a user.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <value>The list of available groups.</value>
+        List<GroupInfoView> ListGroups(SessionInfo user);
+
+        /// <summary>
+        /// Handle a request by a user in a group.
+        /// </summary>
+        /// <param name="user">The session.</param>
+        /// <param name="request">The request.</param>
+        void HandleRequest(SessionInfo user, SyncplayRequestInfo request);
+
+        /// <summary>
+        /// Maps a user to a group.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <param name="group">The group.</param>
+        /// <exception cref="InvalidOperationException"></exception>
+        void MapUserToGroup(SessionInfo user, ISyncplayController group);
+
+        /// <summary>
+        /// Unmaps a user from a group.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <param name="group">The group.</param>
+        /// <exception cref="InvalidOperationException"></exception>
+        void UnmapUserFromGroup(SessionInfo user, ISyncplayController group);
+    }
+}

+ 38 - 0
MediaBrowser.Model/Syncplay/GroupInfoModel.cs

@@ -0,0 +1,38 @@
+namespace MediaBrowser.Model.Syncplay
+{
+    /// <summary>
+    /// Class GroupInfoModel.
+    /// </summary>
+    public class GroupInfoView
+    {
+        /// <summary>
+        /// Gets or sets the group identifier.
+        /// </summary>
+        /// <value>The group identifier.</value>
+        public string GroupId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the playing item id.
+        /// </summary>
+        /// <value>The playing item id.</value>
+        public string PlayingItemId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the playing item name.
+        /// </summary>
+        /// <value>The playing item name.</value>
+        public string PlayingItemName { get; set; }
+
+        /// <summary>
+        /// Gets or sets the position ticks.
+        /// </summary>
+        /// <value>The position ticks.</value>
+        public long PositionTicks { get; set; }
+
+        /// <summary>
+        /// Gets or sets the partecipants.
+        /// </summary>
+        /// <value>The partecipants.</value>
+        public string[] Partecipants { get; set; }
+    }
+}

+ 32 - 0
MediaBrowser.Model/Syncplay/SyncplayCommand.cs

@@ -0,0 +1,32 @@
+namespace MediaBrowser.Model.Syncplay
+{
+    /// <summary>
+    /// Class SyncplayCommand.
+    /// </summary>
+    public class SyncplayCommand
+    {
+        /// <summary>
+        /// Gets or sets the group identifier.
+        /// </summary>
+        /// <value>The group identifier.</value>
+        public string GroupId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the UTC time when to execute the command.
+        /// </summary>
+        /// <value>The UTC time when to execute the command.</value>
+        public string When { get; set; }
+
+        /// <summary>
+        /// Gets or sets the position ticks.
+        /// </summary>
+        /// <value>The position ticks.</value>
+        public long? PositionTicks { get; set; }
+
+        /// <summary>
+        /// Gets or sets the command.
+        /// </summary>
+        /// <value>The command.</value>
+        public SyncplayCommandType Command { get; set; }
+    }
+}

+ 21 - 0
MediaBrowser.Model/Syncplay/SyncplayCommandType.cs

@@ -0,0 +1,21 @@
+namespace MediaBrowser.Model.Syncplay
+{
+    /// <summary>
+    /// Enum SyncplayCommandType.
+    /// </summary>
+    public enum SyncplayCommandType
+    {
+        /// <summary>
+        /// The play command. Instructs users to start playback.
+        /// </summary>
+        Play = 0,
+        /// <summary>
+        /// The pause command. Instructs users to pause playback.
+        /// </summary>
+        Pause = 1,
+        /// <summary>
+        /// The seek command. Instructs users to seek to a specified time.
+        /// </summary>
+        Seek = 2
+    }
+}

+ 26 - 0
MediaBrowser.Model/Syncplay/SyncplayGroupUpdate.cs

@@ -0,0 +1,26 @@
+namespace MediaBrowser.Model.Syncplay
+{
+    /// <summary>
+    /// Class SyncplayGroupUpdate.
+    /// </summary>
+    public class SyncplayGroupUpdate<T>
+    {
+        /// <summary>
+        /// Gets or sets the group identifier.
+        /// </summary>
+        /// <value>The group identifier.</value>
+        public string GroupId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the update type.
+        /// </summary>
+        /// <value>The update type.</value>
+        public SyncplayGroupUpdateType Type { get; set; }
+
+        /// <summary>
+        /// Gets or sets the data.
+        /// </summary>
+        /// <value>The data.</value>
+        public T Data { get; set; }
+    }
+}

+ 41 - 0
MediaBrowser.Model/Syncplay/SyncplayGroupUpdateType.cs

@@ -0,0 +1,41 @@
+namespace MediaBrowser.Model.Syncplay
+{
+    /// <summary>
+    /// Enum SyncplayGroupUpdateType
+    /// </summary>
+    public enum SyncplayGroupUpdateType
+    {
+        /// <summary>
+        /// The user-joined update. Tells members of a group about a new user.
+        /// </summary>
+        UserJoined = 0,
+        /// <summary>
+        /// The user-left update. Tells members of a group that a user left.
+        /// </summary>
+        UserLeft = 1,
+        /// <summary>
+        /// The group-joined update. Tells a user that the group has been joined.
+        /// </summary>
+        GroupJoined = 2,
+        /// <summary>
+        /// The group-left update. Tells a user that the group has been left.
+        /// </summary>
+        GroupLeft = 3,
+        /// <summary>
+        /// The group-wait update. Tells members of the group that a user is buffering.
+        /// </summary>
+        GroupWait = 4,
+        /// <summary>
+        /// The prepare-session update. Tells a user to load some content.
+        /// </summary>
+        PrepareSession = 5,
+        /// <summary>
+        /// The keep-alive update. An update to keep alive the socket.
+        /// </summary>
+        KeepAlive = 6,
+        /// <summary>
+        /// The not-in-group update. Tells a user that no group has been joined.
+        /// </summary>
+        NotInGroup = 7
+    }
+}

+ 34 - 0
MediaBrowser.Model/Syncplay/SyncplayRequestInfo.cs

@@ -0,0 +1,34 @@
+using System;
+
+namespace MediaBrowser.Model.Syncplay
+{
+    /// <summary>
+    /// Class SyncplayRequestInfo.
+    /// </summary>
+    public class SyncplayRequestInfo
+    {
+        /// <summary>
+        /// Gets or sets the request type.
+        /// </summary>
+        /// <value>The request type.</value>
+        public SyncplayRequestType Type;
+
+        /// <summary>
+        /// Gets or sets when the request has been made by the client.
+        /// </summary>
+        /// <value>The date of the request.</value>
+        public DateTime? When { get; set; }
+
+        /// <summary>
+        /// Gets or sets the position ticks.
+        /// </summary>
+        /// <value>The position ticks.</value>
+        public long? PositionTicks { get; set; }
+
+        /// <summary>
+        /// Gets or sets the ping time.
+        /// </summary>
+        /// <value>The ping time.</value>
+        public long? Ping { get; set; }
+    }
+}

+ 33 - 0
MediaBrowser.Model/Syncplay/SyncplayRequestType.cs

@@ -0,0 +1,33 @@
+namespace MediaBrowser.Model.Syncplay
+{
+    /// <summary>
+    /// Enum SyncplayRequestType
+    /// </summary>
+    public enum SyncplayRequestType
+    {
+        /// <summary>
+        /// A user is requesting a play command for the group.
+        /// </summary>
+        Play = 0,
+        /// <summary>
+        /// A user is requesting a pause command for the group.
+        /// </summary>
+        Pause = 1,
+        /// <summary>
+        /// A user is requesting a seek command for the group.
+        /// </summary>
+        Seek = 2,
+        /// <summary>
+        /// A user is signaling that playback is buffering.
+        /// </summary>
+        Buffering = 3,
+        /// <summary>
+        /// A user is signaling that playback resumed.
+        /// </summary>
+        BufferingComplete = 4,
+        /// <summary>
+        /// A user is reporting its ping.
+        /// </summary>
+        KeepAlive = 5
+    }
+}