Sfoglia il codice sorgente

Merge pull request #2162 from Bond-009/websocket

Rewrite WebSocket handling code
Vasily 5 anni fa
parent
commit
5ce008e02f
26 ha cambiato i file con 362 aggiunte e 1202 eliminazioni
  1. 4 1
      Emby.Dlna/PlayTo/PlayToController.cs
  2. 3 30
      Emby.Server.Implementations/ApplicationHost.cs
  3. 74 109
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  4. 0 39
      Emby.Server.Implementations/HttpServer/IHttpListener.cs
  5. 124 155
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  6. 0 39
      Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs
  7. 0 48
      Emby.Server.Implementations/Net/IWebSocket.cs
  8. 0 29
      Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs
  9. 0 191
      Emby.Server.Implementations/Session/HttpSessionController.cs
  10. 7 8
      Emby.Server.Implementations/Session/SessionManager.cs
  11. 19 19
      Emby.Server.Implementations/Session/SessionWebSocketListener.cs
  12. 51 35
      Emby.Server.Implementations/Session/WebSocketController.cs
  13. 0 105
      Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs
  14. 0 135
      Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs
  15. 0 10
      Emby.Server.Implementations/WebSockets/WebSocketHandler.cs
  16. 0 102
      Emby.Server.Implementations/WebSockets/WebSocketManager.cs
  17. 0 1
      Jellyfin.Server/Startup.cs
  18. 22 21
      MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs
  19. 8 8
      MediaBrowser.Api/System/ActivityLogWebSocketListener.cs
  20. 0 2
      MediaBrowser.Controller/IServerApplicationHost.cs
  21. 5 7
      MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
  22. 4 23
      MediaBrowser.Controller/Net/IHttpServer.cs
  23. 9 32
      MediaBrowser.Controller/Net/IWebSocketConnection.cs
  24. 2 1
      MediaBrowser.Controller/Session/ISessionController.cs
  25. 24 50
      MediaBrowser.Controller/Session/SessionInfo.cs
  26. 6 2
      MediaBrowser.Model/Net/WebSocketMessage.cs

+ 4 - 1
Emby.Dlna/PlayTo/PlayToController.cs

@@ -908,7 +908,8 @@ namespace Emby.Dlna.PlayTo
             return 0;
         }
 
-        public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
+        /// <inheritdoc />
+        public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
         {
             if (_disposed)
             {
@@ -924,10 +925,12 @@ namespace Emby.Dlna.PlayTo
             {
                 return SendPlayCommand(data as PlayRequest, cancellationToken);
             }
+
             if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
             {
                 return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
             }
+
             if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
             {
                 return SendGeneralCommand(data as GeneralCommand, cancellationToken);

+ 3 - 30
Emby.Server.Implementations/ApplicationHost.cs

@@ -44,7 +44,6 @@ using Emby.Server.Implementations.Security;
 using Emby.Server.Implementations.Serialization;
 using Emby.Server.Implementations.Services;
 using Emby.Server.Implementations.Session;
-using Emby.Server.Implementations.SocketSharp;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.Updates;
 using MediaBrowser.Api;
@@ -101,11 +100,10 @@ using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.WebDashboard.Api;
 using MediaBrowser.XbmcMetadata.Providers;
 using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
-using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
 using Prometheus.DotNetRuntime;
+using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
 
 namespace Emby.Server.Implementations
 {
@@ -502,32 +500,8 @@ namespace Emby.Server.Implementations
             RegisterServices(serviceCollection);
         }
 
-        public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next)
-        {
-            if (!context.WebSockets.IsWebSocketRequest)
-            {
-                await next().ConfigureAwait(false);
-                return;
-            }
-
-            await _httpServer.ProcessWebSocketRequest(context).ConfigureAwait(false);
-        }
-
-        public async Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
-        {
-            if (context.WebSockets.IsWebSocketRequest)
-            {
-                await next().ConfigureAwait(false);
-                return;
-            }
-
-            var request = context.Request;
-            var response = context.Response;
-            var localPath = context.Request.Path.ToString();
-
-            var req = new WebSocketSharpRequest(request, response, request.Path, LoggerFactory.CreateLogger<WebSocketSharpRequest>());
-            await _httpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false);
-        }
+        public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
+            => _httpServer.RequestHandler(context);
 
         /// <summary>
         /// Registers services/resources with the service collection that will be available via DI.
@@ -613,7 +587,6 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
 
             serviceCollection.AddSingleton<ServiceController>();
-            serviceCollection.AddSingleton<IHttpListener, WebSocketSharpListener>();
             serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
 
             serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();

+ 74 - 109
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -6,11 +6,12 @@ using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Net.Sockets;
+using System.Net.WebSockets;
 using System.Reflection;
 using System.Threading;
 using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
 using Emby.Server.Implementations.Services;
+using Emby.Server.Implementations.SocketSharp;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
@@ -22,6 +23,7 @@ using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Services;
 using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
 using Microsoft.AspNetCore.WebUtilities;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Hosting;
@@ -30,7 +32,7 @@ using ServiceStack.Text.Jsv;
 
 namespace Emby.Server.Implementations.HttpServer
 {
-    public class HttpListenerHost : IHttpServer, IDisposable
+    public class HttpListenerHost : IHttpServer
     {
         /// <summary>
         /// The key for a setting that specifies the default redirect path
@@ -39,17 +41,17 @@ namespace Emby.Server.Implementations.HttpServer
         public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
 
         private readonly ILogger _logger;
+        private readonly ILoggerFactory _loggerFactory;
         private readonly IServerConfigurationManager _config;
         private readonly INetworkManager _networkManager;
         private readonly IServerApplicationHost _appHost;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IXmlSerializer _xmlSerializer;
-        private readonly IHttpListener _socketListener;
         private readonly Func<Type, Func<string, object>> _funcParseFn;
         private readonly string _defaultRedirectPath;
         private readonly string _baseUrlPrefix;
+
         private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
-        private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>();
         private readonly IHostEnvironment _hostEnvironment;
 
         private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
@@ -63,10 +65,10 @@ namespace Emby.Server.Implementations.HttpServer
             INetworkManager networkManager,
             IJsonSerializer jsonSerializer,
             IXmlSerializer xmlSerializer,
-            IHttpListener socketListener,
             ILocalizationManager localizationManager,
             ServiceController serviceController,
-            IHostEnvironment hostEnvironment)
+            IHostEnvironment hostEnvironment,
+            ILoggerFactory loggerFactory)
         {
             _appHost = applicationHost;
             _logger = logger;
@@ -76,11 +78,9 @@ namespace Emby.Server.Implementations.HttpServer
             _networkManager = networkManager;
             _jsonSerializer = jsonSerializer;
             _xmlSerializer = xmlSerializer;
-            _socketListener = socketListener;
             ServiceController = serviceController;
-
-            _socketListener.WebSocketConnected = OnWebSocketConnected;
             _hostEnvironment = hostEnvironment;
+            _loggerFactory = loggerFactory;
 
             _funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
 
@@ -172,38 +172,6 @@ namespace Emby.Server.Implementations.HttpServer
             return attributes;
         }
 
-        private void OnWebSocketConnected(WebSocketConnectEventArgs e)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger)
-            {
-                OnReceive = ProcessWebSocketMessageReceived,
-                Url = e.Url,
-                QueryString = e.QueryString
-            };
-
-            connection.Closed += OnConnectionClosed;
-
-            lock (_webSocketConnections)
-            {
-                _webSocketConnections.Add(connection);
-            }
-
-            WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
-        }
-
-        private void OnConnectionClosed(object sender, EventArgs e)
-        {
-            lock (_webSocketConnections)
-            {
-                _webSocketConnections.Remove((IWebSocketConnection)sender);
-            }
-        }
-
         private static Exception GetActualException(Exception ex)
         {
             if (ex is AggregateException agg)
@@ -289,32 +257,6 @@ namespace Emby.Server.Implementations.HttpServer
                 .Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
         }
 
-        /// <summary>
-        /// Shut down the Web Service
-        /// </summary>
-        public void Stop()
-        {
-            List<IWebSocketConnection> connections;
-
-            lock (_webSocketConnections)
-            {
-                connections = _webSocketConnections.ToList();
-                _webSocketConnections.Clear();
-            }
-
-            foreach (var connection in connections)
-            {
-                try
-                {
-                    connection.Dispose();
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Error disposing connection");
-                }
-            }
-        }
-
         public static string RemoveQueryStringByKey(string url, string key)
         {
             var uri = new Uri(url);
@@ -430,31 +372,46 @@ namespace Emby.Server.Implementations.HttpServer
         /// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns>
         private bool ValidateSsl(string remoteIp, string urlString)
         {
-            if (_config.Configuration.RequireHttps && _appHost.ListenWithHttps)
+            if (_config.Configuration.RequireHttps
+                && _appHost.ListenWithHttps
+                && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase))
             {
-                if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1)
+                // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
+                if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
+                    || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
                 {
-                    // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
-                    if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
-                        || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
-                    {
-                        return true;
-                    }
+                    return true;
+                }
 
-                    if (!_networkManager.IsInLocalNetwork(remoteIp))
-                    {
-                        return false;
-                    }
+                if (!_networkManager.IsInLocalNetwork(remoteIp))
+                {
+                    return false;
                 }
             }
 
             return true;
         }
 
+        /// <inheritdoc />
+        public Task RequestHandler(HttpContext context)
+        {
+            if (context.WebSockets.IsWebSocketRequest)
+            {
+                return WebSocketRequestHandler(context);
+            }
+
+            var request = context.Request;
+            var response = context.Response;
+            var localPath = context.Request.Path.ToString();
+
+            var req = new WebSocketSharpRequest(request, response, request.Path, _logger);
+            return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted);
+        }
+
         /// <summary>
         /// Overridable method that can be used to implement a custom handler.
         /// </summary>
-        public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
+        private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
         {
             var stopWatch = new Stopwatch();
             stopWatch.Start();
@@ -582,6 +539,43 @@ namespace Emby.Server.Implementations.HttpServer
             }
         }
 
+        private async Task WebSocketRequestHandler(HttpContext context)
+        {
+            if (_disposed)
+            {
+                return;
+            }
+
+            try
+            {
+                _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
+
+                WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
+
+                var connection = new WebSocketConnection(
+                    _loggerFactory.CreateLogger<WebSocketConnection>(),
+                    webSocket,
+                    context.Connection.RemoteIpAddress,
+                    context.Request.Query)
+                {
+                    OnReceive = ProcessWebSocketMessageReceived
+                };
+
+                WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
+
+                await connection.ProcessAsync().ConfigureAwait(false);
+                _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
+            }
+            catch (Exception ex) // Otherwise ASP.Net will ignore the exception
+            {
+                _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress);
+                if (!context.Response.HasStarted)
+                {
+                    context.Response.StatusCode = 500;
+                }
+            }
+        }
+
         // Entry point for HttpListener
         public ServiceHandler GetServiceHandler(IHttpRequest httpReq)
         {
@@ -689,11 +683,6 @@ namespace Emby.Server.Implementations.HttpServer
             return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
         }
 
-        public Task ProcessWebSocketRequest(HttpContext context)
-        {
-            return _socketListener.ProcessWebSocketRequest(context);
-        }
-
         private string NormalizeEmbyRoutePath(string path)
         {
             _logger.LogDebug("Normalizing /emby route");
@@ -712,28 +701,6 @@ namespace Emby.Server.Implementations.HttpServer
             return _baseUrlPrefix + NormalizeUrlPath(path);
         }
 
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        protected virtual void Dispose(bool disposing)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            if (disposing)
-            {
-                Stop();
-            }
-
-            _disposed = true;
-        }
-
         /// <summary>
         /// Processes the web socket message received.
         /// </summary>
@@ -745,8 +712,6 @@ namespace Emby.Server.Implementations.HttpServer
                 return Task.CompletedTask;
             }
 
-            _logger.LogDebug("Websocket message received: {0}", result.MessageType);
-
             IEnumerable<Task> GetTasks()
             {
                 foreach (var x in _webSocketListeners)

+ 0 - 39
Emby.Server.Implementations/HttpServer/IHttpListener.cs

@@ -1,39 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-
-namespace Emby.Server.Implementations.HttpServer
-{
-    public interface IHttpListener : IDisposable
-    {
-        /// <summary>
-        /// Gets or sets the error handler.
-        /// </summary>
-        /// <value>The error handler.</value>
-        Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
-
-        /// <summary>
-        /// Gets or sets the request handler.
-        /// </summary>
-        /// <value>The request handler.</value>
-        Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
-
-        /// <summary>
-        /// Gets or sets the web socket handler.
-        /// </summary>
-        /// <value>The web socket handler.</value>
-        Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
-
-        /// <summary>
-        /// Stops this instance.
-        /// </summary>
-        Task Stop();
-
-        Task ProcessWebSocketRequest(HttpContext ctx);
-    }
-}

+ 124 - 155
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -1,15 +1,18 @@
-using System;
+#nullable enable
+
+using System;
+using System.Buffers;
+using System.IO.Pipelines;
+using System.Net;
 using System.Net.WebSockets;
-using System.Text;
+using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
+using MediaBrowser.Common.Json;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
-using UtfUnknown;
 
 namespace Emby.Server.Implementations.HttpServer
 {
@@ -24,69 +27,50 @@ namespace Emby.Server.Implementations.HttpServer
         private readonly ILogger _logger;
 
         /// <summary>
-        /// The json serializer.
+        /// The json serializer options.
         /// </summary>
-        private readonly IJsonSerializer _jsonSerializer;
+        private readonly JsonSerializerOptions _jsonOptions;
 
         /// <summary>
         /// The socket.
         /// </summary>
-        private readonly IWebSocket _socket;
+        private readonly WebSocket _socket;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
         /// </summary>
+        /// <param name="logger">The logger.</param>
         /// <param name="socket">The socket.</param>
         /// <param name="remoteEndPoint">The remote end point.</param>
-        /// <param name="jsonSerializer">The json serializer.</param>
-        /// <param name="logger">The logger.</param>
-        /// <exception cref="ArgumentNullException">socket</exception>
-        public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger)
+        /// <param name="query">The query.</param>
+        public WebSocketConnection(
+            ILogger<WebSocketConnection> logger,
+            WebSocket socket,
+            IPAddress? remoteEndPoint,
+            IQueryCollection query)
         {
-            if (socket == null)
-            {
-                throw new ArgumentNullException(nameof(socket));
-            }
-
-            if (string.IsNullOrEmpty(remoteEndPoint))
-            {
-                throw new ArgumentNullException(nameof(remoteEndPoint));
-            }
-
-            if (jsonSerializer == null)
-            {
-                throw new ArgumentNullException(nameof(jsonSerializer));
-            }
-
-            if (logger == null)
-            {
-                throw new ArgumentNullException(nameof(logger));
-            }
-
-            Id = Guid.NewGuid();
-            _jsonSerializer = jsonSerializer;
+            _logger = logger;
             _socket = socket;
-            _socket.OnReceiveBytes = OnReceiveInternal;
-
             RemoteEndPoint = remoteEndPoint;
-            _logger = logger;
+            QueryString = query;
 
-            socket.Closed += OnSocketClosed;
+            _jsonOptions = JsonDefaults.GetOptions();
+            LastActivityDate = DateTime.Now;
         }
 
         /// <inheritdoc />
-        public event EventHandler<EventArgs> Closed;
+        public event EventHandler<EventArgs>? Closed;
 
         /// <summary>
         /// Gets or sets the remote end point.
         /// </summary>
-        public string RemoteEndPoint { get; private set; }
+        public IPAddress? RemoteEndPoint { get; }
 
         /// <summary>
         /// Gets or sets the receive action.
         /// </summary>
         /// <value>The receive action.</value>
-        public Func<WebSocketMessageInfo, Task> OnReceive { get; set; }
+        public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
 
         /// <summary>
         /// Gets the last activity date.
@@ -94,23 +78,11 @@ namespace Emby.Server.Implementations.HttpServer
         /// <value>The last activity date.</value>
         public DateTime LastActivityDate { get; private set; }
 
-        /// <summary>
-        /// Gets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        public Guid Id { get; private set; }
-
-        /// <summary>
-        /// Gets or sets the URL.
-        /// </summary>
-        /// <value>The URL.</value>
-        public string Url { get; set; }
-
         /// <summary>
         /// Gets or sets the query string.
         /// </summary>
         /// <value>The query string.</value>
-        public IQueryCollection QueryString { get; set; }
+        public IQueryCollection QueryString { get; }
 
         /// <summary>
         /// Gets the state.
@@ -118,138 +90,135 @@ namespace Emby.Server.Implementations.HttpServer
         /// <value>The state.</value>
         public WebSocketState State => _socket.State;
 
-        void OnSocketClosed(object sender, EventArgs e)
+        /// <summary>
+        /// Sends a message asynchronously.
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <param name="message">The message.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
         {
-            Closed?.Invoke(this, EventArgs.Empty);
+            var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
+            return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
         }
 
-        /// <summary>
-        /// Called when [receive].
-        /// </summary>
-        /// <param name="bytes">The bytes.</param>
-        private void OnReceiveInternal(byte[] bytes)
+        /// <inheritdoc />
+        public async Task ProcessAsync(CancellationToken cancellationToken = default)
         {
-            LastActivityDate = DateTime.UtcNow;
+            var pipe = new Pipe();
+            var writer = pipe.Writer;
 
-            if (OnReceive == null)
+            ValueWebSocketReceiveResult receiveresult;
+            do
             {
-                return;
-            }
-            var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName;
+                // Allocate at least 512 bytes from the PipeWriter
+                Memory<byte> memory = writer.GetMemory(512);
+                try
+                {
+                    receiveresult = await _socket.ReceiveAsync(memory, cancellationToken);
+                }
+                catch (WebSocketException ex)
+                {
+                    _logger.LogWarning("WS {IP} error receiving data: {Message}", RemoteEndPoint, ex.Message);
+                    break;
+                }
 
-            if (string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase))
-            {
-                OnReceiveInternal(Encoding.UTF8.GetString(bytes, 0, bytes.Length));
-            }
-            else
+                int bytesRead = receiveresult.Count;
+                if (bytesRead == 0)
+                {
+                    break;
+                }
+
+                // Tell the PipeWriter how much was read from the Socket
+                writer.Advance(bytesRead);
+
+                // Make the data available to the PipeReader
+                FlushResult flushResult = await writer.FlushAsync();
+                if (flushResult.IsCompleted)
+                {
+                    // The PipeReader stopped reading
+                    break;
+                }
+
+                LastActivityDate = DateTime.UtcNow;
+
+                if (receiveresult.EndOfMessage)
+                {
+                    await ProcessInternal(pipe.Reader).ConfigureAwait(false);
+                }
+            } while (
+                (_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
+                && receiveresult.MessageType != WebSocketMessageType.Close);
+
+            Closed?.Invoke(this, EventArgs.Empty);
+
+            if (_socket.State == WebSocketState.Open
+                || _socket.State == WebSocketState.CloseReceived
+                || _socket.State == WebSocketState.CloseSent)
             {
-                OnReceiveInternal(Encoding.ASCII.GetString(bytes, 0, bytes.Length));
+                await _socket.CloseAsync(
+                    WebSocketCloseStatus.NormalClosure,
+                    string.Empty,
+                    cancellationToken).ConfigureAwait(false);
             }
         }
 
-        private void OnReceiveInternal(string message)
+        private async Task ProcessInternal(PipeReader reader)
         {
-            LastActivityDate = DateTime.UtcNow;
-
-            if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
-            {
-                // This info is useful sometimes but also clogs up the log
-                _logger.LogDebug("Received web socket message that is not a json structure: {message}", message);
-                return;
-            }
+            ReadResult result = await reader.ReadAsync().ConfigureAwait(false);
+            ReadOnlySequence<byte> buffer = result.Buffer;
 
             if (OnReceive == null)
             {
+                // Tell the PipeReader how much of the buffer we have consumed
+                reader.AdvanceTo(buffer.End);
                 return;
             }
 
+            WebSocketMessage<object> stub;
             try
             {
-                var stub = (WebSocketMessage<object>)_jsonSerializer.DeserializeFromString(message, typeof(WebSocketMessage<object>));
 
-                var info = new WebSocketMessageInfo
+                if (buffer.IsSingleSegment)
                 {
-                    MessageType = stub.MessageType,
-                    Data = stub.Data?.ToString(),
-                    Connection = this
-                };
-
-                OnReceive(info);
+                    stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buffer.FirstSpan, _jsonOptions);
+                }
+                else
+                {
+                    var buf = ArrayPool<byte>.Shared.Rent(Convert.ToInt32(buffer.Length));
+                    try
+                    {
+                        buffer.CopyTo(buf);
+                        stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buf, _jsonOptions);
+                    }
+                    finally
+                    {
+                        ArrayPool<byte>.Shared.Return(buf);
+                    }
+                }
             }
-            catch (Exception ex)
+            catch (JsonException ex)
             {
+                // Tell the PipeReader how much of the buffer we have consumed
+                reader.AdvanceTo(buffer.End);
                 _logger.LogError(ex, "Error processing web socket message");
-            }
-        }
-
-        /// <summary>
-        /// Sends a message asynchronously.
-        /// </summary>
-        /// <typeparam name="T"></typeparam>
-        /// <param name="message">The message.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        /// <exception cref="ArgumentNullException">message</exception>
-        public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
-        {
-            if (message == null)
-            {
-                throw new ArgumentNullException(nameof(message));
-            }
-
-            var json = _jsonSerializer.SerializeToString(message);
-
-            return SendAsync(json, cancellationToken);
-        }
-
-        /// <summary>
-        /// Sends a message asynchronously.
-        /// </summary>
-        /// <param name="buffer">The buffer.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public Task SendAsync(byte[] buffer, CancellationToken cancellationToken)
-        {
-            if (buffer == null)
-            {
-                throw new ArgumentNullException(nameof(buffer));
+                return;
             }
 
-            cancellationToken.ThrowIfCancellationRequested();
+            // Tell the PipeReader how much of the buffer we have consumed
+            reader.AdvanceTo(buffer.End);
 
-            return _socket.SendAsync(buffer, true, cancellationToken);
-        }
+            _logger.LogDebug("WS {IP} received message: {@Message}", RemoteEndPoint, stub);
 
-        /// <inheritdoc />
-        public Task SendAsync(string text, CancellationToken cancellationToken)
-        {
-            if (string.IsNullOrEmpty(text))
+            var info = new WebSocketMessageInfo
             {
-                throw new ArgumentNullException(nameof(text));
-            }
+                MessageType = stub.MessageType,
+                Data = stub.Data?.ToString(), // Data can be null
+                Connection = this
+            };
 
-            cancellationToken.ThrowIfCancellationRequested();
-
-            return _socket.SendAsync(text, true, cancellationToken);
-        }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        /// <summary>
-        /// Releases unmanaged and - optionally - managed resources.
-        /// </summary>
-        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
-        protected virtual void Dispose(bool dispose)
-        {
-            if (dispose)
-            {
-                _socket.Dispose();
-            }
+            await OnReceive(info).ConfigureAwait(false);
         }
     }
 }

+ 0 - 39
Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs

@@ -1,39 +0,0 @@
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-using WebSocketManager = Emby.Server.Implementations.WebSockets.WebSocketManager;
-
-namespace Emby.Server.Implementations.Middleware
-{
-    public class WebSocketMiddleware
-    {
-        private readonly RequestDelegate _next;
-        private readonly ILogger<WebSocketMiddleware> _logger;
-        private readonly WebSocketManager _webSocketManager;
-
-        public WebSocketMiddleware(RequestDelegate next, ILogger<WebSocketMiddleware> logger, WebSocketManager webSocketManager)
-        {
-            _next = next;
-            _logger = logger;
-            _webSocketManager = webSocketManager;
-        }
-
-        public async Task Invoke(HttpContext httpContext)
-        {
-            _logger.LogInformation("Handling request: " + httpContext.Request.Path);
-
-            if (httpContext.WebSockets.IsWebSocketRequest)
-            {
-                var webSocketContext = await httpContext.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
-                if (webSocketContext != null)
-                {
-                    await _webSocketManager.OnWebSocketConnected(webSocketContext).ConfigureAwait(false);
-                }
-            }
-            else
-            {
-                await _next.Invoke(httpContext).ConfigureAwait(false);
-            }
-        }
-    }
-}

+ 0 - 48
Emby.Server.Implementations/Net/IWebSocket.cs

@@ -1,48 +0,0 @@
-using System;
-using System.Net.WebSockets;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Emby.Server.Implementations.Net
-{
-    /// <summary>
-    /// Interface IWebSocket
-    /// </summary>
-    public interface IWebSocket : IDisposable
-    {
-        /// <summary>
-        /// Occurs when [closed].
-        /// </summary>
-        event EventHandler<EventArgs> Closed;
-
-        /// <summary>
-        /// Gets or sets the state.
-        /// </summary>
-        /// <value>The state.</value>
-        WebSocketState State { get; }
-
-        /// <summary>
-        /// Gets or sets the receive action.
-        /// </summary>
-        /// <value>The receive action.</value>
-        Action<byte[]> OnReceiveBytes { get; set; }
-
-        /// <summary>
-        /// Sends the async.
-        /// </summary>
-        /// <param name="bytes">The bytes.</param>
-        /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Sends the asynchronous.
-        /// </summary>
-        /// <param name="text">The text.</param>
-        /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken);
-    }
-}

+ 0 - 29
Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs

@@ -1,29 +0,0 @@
-using System;
-using Microsoft.AspNetCore.Http;
-
-namespace Emby.Server.Implementations.Net
-{
-    public class WebSocketConnectEventArgs : EventArgs
-    {
-        /// <summary>
-        /// Gets or sets the URL.
-        /// </summary>
-        /// <value>The URL.</value>
-        public string Url { get; set; }
-        /// <summary>
-        /// Gets or sets the query string.
-        /// </summary>
-        /// <value>The query string.</value>
-        public IQueryCollection QueryString { get; set; }
-        /// <summary>
-        /// Gets or sets the web socket.
-        /// </summary>
-        /// <value>The web socket.</value>
-        public IWebSocket WebSocket { get; set; }
-        /// <summary>
-        /// Gets or sets the endpoint.
-        /// </summary>
-        /// <value>The endpoint.</value>
-        public string Endpoint { get; set; }
-    }
-}

+ 0 - 191
Emby.Server.Implementations/Session/HttpSessionController.cs

@@ -1,191 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Session;
-
-namespace Emby.Server.Implementations.Session
-{
-    public class HttpSessionController : ISessionController
-    {
-        private readonly IHttpClient _httpClient;
-        private readonly IJsonSerializer _json;
-        private readonly ISessionManager _sessionManager;
-
-        public SessionInfo Session { get; private set; }
-
-        private readonly string _postUrl;
-
-        public HttpSessionController(IHttpClient httpClient,
-            IJsonSerializer json,
-            SessionInfo session,
-            string postUrl, ISessionManager sessionManager)
-        {
-            _httpClient = httpClient;
-            _json = json;
-            Session = session;
-            _postUrl = postUrl;
-            _sessionManager = sessionManager;
-        }
-
-        private string PostUrl => string.Format("http://{0}{1}", Session.RemoteEndPoint, _postUrl);
-
-        public bool IsSessionActive => (DateTime.UtcNow - Session.LastActivityDate).TotalMinutes <= 5;
-
-        public bool SupportsMediaControl => true;
-
-        private Task SendMessage(string name, string messageId, CancellationToken cancellationToken)
-        {
-            return SendMessage(name, messageId, new Dictionary<string, string>(), cancellationToken);
-        }
-
-        private Task SendMessage(string name, string messageId, Dictionary<string, string> args, CancellationToken cancellationToken)
-        {
-            args["messageId"] = messageId;
-            var url = PostUrl + "/" + name + ToQueryString(args);
-
-            return SendRequest(new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = cancellationToken,
-                BufferContent = false
-            });
-        }
-
-        private Task SendPlayCommand(PlayRequest command, string messageId, CancellationToken cancellationToken)
-        {
-            var dict = new Dictionary<string, string>();
-
-            dict["ItemIds"] = string.Join(",", command.ItemIds.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
-
-            if (command.StartPositionTicks.HasValue)
-            {
-                dict["StartPositionTicks"] = command.StartPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
-            }
-            if (command.AudioStreamIndex.HasValue)
-            {
-                dict["AudioStreamIndex"] = command.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture);
-            }
-            if (command.SubtitleStreamIndex.HasValue)
-            {
-                dict["SubtitleStreamIndex"] = command.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture);
-            }
-            if (command.StartIndex.HasValue)
-            {
-                dict["StartIndex"] = command.StartIndex.Value.ToString(CultureInfo.InvariantCulture);
-            }
-            if (!string.IsNullOrEmpty(command.MediaSourceId))
-            {
-                dict["MediaSourceId"] = command.MediaSourceId;
-            }
-
-            return SendMessage(command.PlayCommand.ToString(), messageId, dict, cancellationToken);
-        }
-
-        private Task SendPlaystateCommand(PlaystateRequest command, string messageId, CancellationToken cancellationToken)
-        {
-            var args = new Dictionary<string, string>();
-
-            if (command.Command == PlaystateCommand.Seek)
-            {
-                if (!command.SeekPositionTicks.HasValue)
-                {
-                    throw new ArgumentException("SeekPositionTicks cannot be null");
-                }
-
-                args["SeekPositionTicks"] = command.SeekPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
-            }
-
-            return SendMessage(command.Command.ToString(), messageId, args, cancellationToken);
-        }
-
-        private string[] _supportedMessages = Array.Empty<string>();
-        public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
-        {
-            if (!IsSessionActive)
-            {
-                return Task.CompletedTask;
-            }
-
-            if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
-            {
-                return SendPlayCommand(data as PlayRequest, messageId, cancellationToken);
-            }
-            if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
-            {
-                return SendPlaystateCommand(data as PlaystateRequest, messageId, cancellationToken);
-            }
-            if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
-            {
-                var command = data as GeneralCommand;
-                return SendMessage(command.Name, messageId, command.Arguments, cancellationToken);
-            }
-
-            if (!_supportedMessages.Contains(name, StringComparer.OrdinalIgnoreCase))
-            {
-                return Task.CompletedTask;
-            }
-
-            var url = PostUrl + "/" + name;
-
-            url += "?messageId=" + messageId;
-
-            var options = new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = cancellationToken,
-                BufferContent = false
-            };
-
-            if (data != null)
-            {
-                if (typeof(T) == typeof(string))
-                {
-                    var str = data as string;
-                    if (!string.IsNullOrEmpty(str))
-                    {
-                        options.RequestContent = str;
-                        options.RequestContentType = "application/json";
-                    }
-                }
-                else
-                {
-                    options.RequestContent = _json.SerializeToString(data);
-                    options.RequestContentType = "application/json";
-                }
-            }
-
-            return SendRequest(options);
-        }
-
-        private async Task SendRequest(HttpRequestOptions options)
-        {
-            using (var response = await _httpClient.Post(options).ConfigureAwait(false))
-            {
-
-            }
-        }
-
-        private static string ToQueryString(Dictionary<string, string> nvc)
-        {
-            var array = (from item in nvc
-                         select string.Format("{0}={1}", WebUtility.UrlEncode(item.Key), WebUtility.UrlEncode(item.Value)))
-                .ToArray();
-
-            var args = string.Join("&", array);
-
-            if (string.IsNullOrEmpty(args))
-            {
-                return args;
-            }
-
-            return "?" + args;
-        }
-    }
-}

+ 7 - 8
Emby.Server.Implementations/Session/SessionManager.cs

@@ -477,8 +477,7 @@ namespace Emby.Server.Implementations.Session
                 Client = appName,
                 DeviceId = deviceId,
                 ApplicationVersion = appVersion,
-                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
-                ServerId = _appHost.SystemId
+                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture)
             };
 
             var username = user?.Name;
@@ -1042,12 +1041,12 @@ namespace Emby.Server.Implementations.Session
 
         private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken)
         {
-            var controllers = session.SessionControllers.ToArray();
-            var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+            var controllers = session.SessionControllers;
+            var messageId = Guid.NewGuid();
 
             foreach (var controller in controllers)
             {
-                await controller.SendMessage(name, messageId, data, controllers, cancellationToken).ConfigureAwait(false);
+                await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
             }
         }
 
@@ -1055,13 +1054,13 @@ namespace Emby.Server.Implementations.Session
         {
             IEnumerable<Task> GetTasks()
             {
-                var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+                var messageId = Guid.NewGuid();
                 foreach (var session in sessions)
                 {
                     var controllers = session.SessionControllers;
                     foreach (var controller in controllers)
                     {
-                        yield return controller.SendMessage(name, messageId, data, controllers, cancellationToken);
+                        yield return controller.SendMessage(name, messageId, data, cancellationToken);
                     }
                 }
             }
@@ -1762,7 +1761,7 @@ namespace Emby.Server.Implementations.Session
                 throw new ArgumentNullException(nameof(info));
             }
 
-            var user = info.UserId.Equals(Guid.Empty)
+            var user = info.UserId == Guid.Empty
                 ? null
                 : _userManager.GetUserById(info.UserId);
 

+ 19 - 19
Emby.Server.Implementations/Session/SessionWebSocketListener.cs

@@ -3,7 +3,6 @@ using System.Threading.Tasks;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Serialization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 
@@ -12,7 +11,7 @@ namespace Emby.Server.Implementations.Session
     /// <summary>
     /// Class SessionWebSocketListener
     /// </summary>
-    public class SessionWebSocketListener : IWebSocketListener, IDisposable
+    public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable
     {
         /// <summary>
         /// The _session manager
@@ -23,42 +22,41 @@ namespace Emby.Server.Implementations.Session
         /// The _logger
         /// </summary>
         private readonly ILogger _logger;
-
-        /// <summary>
-        /// The _dto service
-        /// </summary>
-        private readonly IJsonSerializer _json;
+        private readonly ILoggerFactory _loggerFactory;
 
         private readonly IHttpServer _httpServer;
 
-
         /// <summary>
         /// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
         /// </summary>
+        /// <param name="logger">The logger.</param>
         /// <param name="sessionManager">The session manager.</param>
         /// <param name="loggerFactory">The logger factory.</param>
-        /// <param name="json">The json.</param>
         /// <param name="httpServer">The HTTP server.</param>
-        public SessionWebSocketListener(ISessionManager sessionManager, ILoggerFactory loggerFactory, IJsonSerializer json, IHttpServer httpServer)
+        public SessionWebSocketListener(
+            ILogger<SessionWebSocketListener> logger,
+            ISessionManager sessionManager,
+            ILoggerFactory loggerFactory,
+            IHttpServer httpServer)
         {
+            _logger = logger;
             _sessionManager = sessionManager;
-            _logger = loggerFactory.CreateLogger(GetType().Name);
-            _json = json;
+            _loggerFactory = loggerFactory;
             _httpServer = httpServer;
-            httpServer.WebSocketConnected += _serverManager_WebSocketConnected;
+
+            httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
         }
 
-        void _serverManager_WebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
+        private 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.ToString());
             if (session != null)
             {
                 EnsureController(session, e.Argument);
             }
             else
             {
-                _logger.LogWarning("Unable to determine session based on url: {0}", e.Argument.Url);
+                _logger.LogWarning("Unable to determine session based on query string: {0}", e.Argument.QueryString);
             }
         }
 
@@ -79,9 +77,10 @@ namespace Emby.Server.Implementations.Session
             return _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint);
         }
 
+        /// <inheritdoc />
         public void Dispose()
         {
-            _httpServer.WebSocketConnected -= _serverManager_WebSocketConnected;
+            _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
         }
 
         /// <summary>
@@ -94,7 +93,8 @@ namespace Emby.Server.Implementations.Session
 
         private void EnsureController(SessionInfo session, IWebSocketConnection connection)
         {
-            var controllerInfo = session.EnsureController<WebSocketController>(s => new WebSocketController(s, _logger, _sessionManager));
+            var controllerInfo = session.EnsureController<WebSocketController>(
+                s => new WebSocketController(_loggerFactory.CreateLogger<WebSocketController>(), s, _sessionManager));
 
             var controller = (WebSocketController)controllerInfo.Item1;
             controller.AddWebSocket(connection);

+ 51 - 35
Emby.Server.Implementations/Session/WebSocketController.cs

@@ -1,3 +1,7 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+#nullable enable
+
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -11,60 +15,63 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Session
 {
-    public class WebSocketController : ISessionController, IDisposable
+    public sealed class WebSocketController : ISessionController, IDisposable
     {
-        public SessionInfo Session { get; private set; }
-        public IReadOnlyList<IWebSocketConnection> Sockets { get; private set; }
-
         private readonly ILogger _logger;
-
         private readonly ISessionManager _sessionManager;
+        private readonly SessionInfo _session;
 
-        public WebSocketController(SessionInfo session, ILogger logger, ISessionManager sessionManager)
+        private readonly List<IWebSocketConnection> _sockets;
+        private bool _disposed = false;
+
+        public WebSocketController(
+            ILogger<WebSocketController> logger,
+            SessionInfo session,
+            ISessionManager sessionManager)
         {
-            Session = session;
             _logger = logger;
+            _session = session;
             _sessionManager = sessionManager;
-            Sockets = new List<IWebSocketConnection>();
+            _sockets = new List<IWebSocketConnection>();
         }
 
         private bool HasOpenSockets => GetActiveSockets().Any();
 
+        /// <inheritdoc />
         public bool SupportsMediaControl => HasOpenSockets;
 
+        /// <inheritdoc />
         public bool IsSessionActive => HasOpenSockets;
 
         private IEnumerable<IWebSocketConnection> GetActiveSockets()
-        {
-            return Sockets
-                .OrderByDescending(i => i.LastActivityDate)
-                .Where(i => i.State == WebSocketState.Open);
-        }
+            => _sockets.Where(i => i.State == WebSocketState.Open);
 
         public void AddWebSocket(IWebSocketConnection connection)
         {
-            var sockets = Sockets.ToList();
-            sockets.Add(connection);
+            _logger.LogDebug("Adding websocket to session {Session}", _session.Id);
+            _sockets.Add(connection);
 
-            Sockets = sockets;
-
-            connection.Closed += connection_Closed;
+            connection.Closed += OnConnectionClosed;
         }
 
-        void connection_Closed(object sender, EventArgs e)
+        private void OnConnectionClosed(object sender, EventArgs e)
         {
             var connection = (IWebSocketConnection)sender;
-            var sockets = Sockets.ToList();
-            sockets.Remove(connection);
-
-            Sockets = sockets;
-
-            _sessionManager.CloseIfNeeded(Session);
+            _logger.LogDebug("Removing websocket from session {Session}", _session.Id);
+            _sockets.Remove(connection);
+            connection.Closed -= OnConnectionClosed;
+            _sessionManager.CloseIfNeeded(_session);
         }
 
-        public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
+        /// <inheritdoc />
+        public Task SendMessage<T>(
+            string name,
+            Guid messageId,
+            T data,
+            CancellationToken cancellationToken)
         {
             var socket = GetActiveSockets()
+                .OrderByDescending(i => i.LastActivityDate)
                 .FirstOrDefault();
 
             if (socket == null)
@@ -72,21 +79,30 @@ namespace Emby.Server.Implementations.Session
                 return Task.CompletedTask;
             }
 
-            return socket.SendAsync(new WebSocketMessage<T>
-            {
-                Data = data,
-                MessageType = name,
-                MessageId = messageId
-
-            }, cancellationToken);
+            return socket.SendAsync(
+                new WebSocketMessage<T>
+                {
+                    Data = data,
+                    MessageType = name,
+                    MessageId = messageId
+                },
+                cancellationToken);
         }
 
+        /// <inheritdoc />
         public void Dispose()
         {
-            foreach (var socket in Sockets.ToList())
+            if (_disposed)
             {
-                socket.Closed -= connection_Closed;
+                return;
             }
+
+            foreach (var socket in _sockets)
+            {
+                socket.Closed -= OnConnectionClosed;
+            }
+
+            _disposed = true;
         }
     }
 }

+ 0 - 105
Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs

@@ -1,105 +0,0 @@
-using System;
-using System.Net.WebSockets;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.SocketSharp
-{
-    public class SharpWebSocket : IWebSocket
-    {
-        /// <summary>
-        /// The logger
-        /// </summary>
-        private readonly ILogger _logger;
-
-        public event EventHandler<EventArgs> Closed;
-
-        /// <summary>
-        /// Gets or sets the web socket.
-        /// </summary>
-        /// <value>The web socket.</value>
-        private readonly WebSocket _webSocket;
-
-        private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
-        private bool _disposed;
-
-        public SharpWebSocket(WebSocket socket, ILogger logger)
-        {
-            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
-            _webSocket = socket ?? throw new ArgumentNullException(nameof(socket));
-        }
-
-        /// <summary>
-        /// Gets the state.
-        /// </summary>
-        /// <value>The state.</value>
-        public WebSocketState State => _webSocket.State;
-
-        /// <summary>
-        /// Sends the async.
-        /// </summary>
-        /// <param name="bytes">The bytes.</param>
-        /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken)
-        {
-            return _webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Binary, endOfMessage, cancellationToken);
-        }
-
-        /// <summary>
-        /// Sends the asynchronous.
-        /// </summary>
-        /// <param name="text">The text.</param>
-        /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken)
-        {
-            return _webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(text)), WebSocketMessageType.Text, endOfMessage, cancellationToken);
-        }
-
-        /// <summary>
-        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
-        /// </summary>
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        /// <summary>
-        /// Releases unmanaged and - optionally - managed resources.
-        /// </summary>
-        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
-        protected virtual void Dispose(bool dispose)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            if (dispose)
-            {
-                _cancellationTokenSource.Cancel();
-                if (_webSocket.State == WebSocketState.Open)
-                {
-                    _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client",
-                        CancellationToken.None);
-                }
-                Closed?.Invoke(this, EventArgs.Empty);
-            }
-
-            _disposed = true;
-        }
-
-        /// <summary>
-        /// Gets or sets the receive action.
-        /// </summary>
-        /// <value>The receive action.</value>
-        public Action<byte[]> OnReceiveBytes { get; set; }
-    }
-}

+ 0 - 135
Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs

@@ -1,135 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.WebSockets;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-using Emby.Server.Implementations.Net;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.SocketSharp
-{
-    public class WebSocketSharpListener : IHttpListener
-    {
-        private readonly ILogger _logger;
-
-        private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
-        private CancellationToken _disposeCancellationToken;
-
-        public WebSocketSharpListener(ILogger<WebSocketSharpListener> logger)
-        {
-            _logger = logger;
-            _disposeCancellationToken = _disposeCancellationTokenSource.Token;
-        }
-
-        public Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
-
-        public Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
-
-        public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
-
-        private static void LogRequest(ILogger logger, HttpRequest request)
-        {
-            var url = request.GetDisplayUrl();
-
-            logger.LogInformation("WS {Url}. UserAgent: {UserAgent}", url, request.Headers[HeaderNames.UserAgent].ToString());
-        }
-
-        public async Task ProcessWebSocketRequest(HttpContext ctx)
-        {
-            try
-            {
-                LogRequest(_logger, ctx.Request);
-                var endpoint = ctx.Connection.RemoteIpAddress.ToString();
-                var url = ctx.Request.GetDisplayUrl();
-
-                var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
-                var socket = new SharpWebSocket(webSocketContext, _logger);
-
-                WebSocketConnected(new WebSocketConnectEventArgs
-                {
-                    Url = url,
-                    QueryString = ctx.Request.Query,
-                    WebSocket = socket,
-                    Endpoint = endpoint
-                });
-
-                WebSocketReceiveResult result;
-                var message = new List<byte>();
-
-                do
-                {
-                    var buffer = WebSocket.CreateServerBuffer(4096);
-                    result = await webSocketContext.ReceiveAsync(buffer, _disposeCancellationToken);
-                    message.AddRange(buffer.Array.Take(result.Count));
-
-                    if (result.EndOfMessage)
-                    {
-                        socket.OnReceiveBytes(message.ToArray());
-                        message.Clear();
-                    }
-                } while (socket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close);
-
-
-                if (webSocketContext.State == WebSocketState.Open)
-                {
-                    await webSocketContext.CloseAsync(
-                        result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
-                        result.CloseStatusDescription,
-                        _disposeCancellationToken).ConfigureAwait(false);
-                }
-
-                socket.Dispose();
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "AcceptWebSocketAsync error");
-                if (!ctx.Response.HasStarted)
-                {
-                    ctx.Response.StatusCode = 500;
-                }
-            }
-        }
-
-        public Task Stop()
-        {
-            _disposeCancellationTokenSource.Cancel();
-            return Task.CompletedTask;
-        }
-
-        /// <summary>
-        /// Releases the unmanaged resources and disposes of the managed resources used.
-        /// </summary>
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        private bool _disposed;
-
-        /// <summary>
-        /// Releases the unmanaged resources and disposes of the managed resources used.
-        /// </summary>
-        /// <param name="disposing">Whether or not the managed resources should be disposed.</param>
-        protected virtual void Dispose(bool disposing)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            if (disposing)
-            {
-                Stop().GetAwaiter().GetResult();
-            }
-
-            _disposed = true;
-        }
-    }
-}

+ 0 - 10
Emby.Server.Implementations/WebSockets/WebSocketHandler.cs

@@ -1,10 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Model.Net;
-
-namespace Emby.Server.Implementations.WebSockets
-{
-    public interface IWebSocketHandler
-    {
-        Task ProcessMessage(WebSocketMessage<object> message, TaskCompletionSource<bool> taskCompletionSource);
-    }
-}

+ 0 - 102
Emby.Server.Implementations/WebSockets/WebSocketManager.cs

@@ -1,102 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.WebSockets;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
-using UtfUnknown;
-
-namespace Emby.Server.Implementations.WebSockets
-{
-    public class WebSocketManager
-    {
-        private readonly IWebSocketHandler[] _webSocketHandlers;
-        private readonly IJsonSerializer _jsonSerializer;
-        private readonly ILogger<WebSocketManager> _logger;
-        private const int BufferSize = 4096;
-
-        public WebSocketManager(IWebSocketHandler[] webSocketHandlers, IJsonSerializer jsonSerializer, ILogger<WebSocketManager> logger)
-        {
-            _webSocketHandlers = webSocketHandlers;
-            _jsonSerializer = jsonSerializer;
-            _logger = logger;
-        }
-
-        public async Task OnWebSocketConnected(WebSocket webSocket)
-        {
-            var taskCompletionSource = new TaskCompletionSource<bool>();
-            var cancellationToken = new CancellationTokenSource().Token;
-            WebSocketReceiveResult result;
-            var message = new List<byte>();
-
-            // Keep listening for incoming messages, otherwise the socket closes automatically
-            do
-            {
-                var buffer = WebSocket.CreateServerBuffer(BufferSize);
-                result = await webSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false);
-                message.AddRange(buffer.Array.Take(result.Count));
-
-                if (result.EndOfMessage)
-                {
-                    await ProcessMessage(message.ToArray(), taskCompletionSource).ConfigureAwait(false);
-                    message.Clear();
-                }
-            } while (!taskCompletionSource.Task.IsCompleted &&
-                     webSocket.State == WebSocketState.Open &&
-                     result.MessageType != WebSocketMessageType.Close);
-
-            if (webSocket.State == WebSocketState.Open)
-            {
-                await webSocket.CloseAsync(
-                    result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
-                    result.CloseStatusDescription,
-                    cancellationToken).ConfigureAwait(false);
-            }
-        }
-
-        private async Task ProcessMessage(byte[] messageBytes, TaskCompletionSource<bool> taskCompletionSource)
-        {
-            var charset = CharsetDetector.DetectFromBytes(messageBytes).Detected?.EncodingName;
-            var message = string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase)
-                ? Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length)
-                : Encoding.ASCII.GetString(messageBytes, 0, messageBytes.Length);
-
-            // All messages are expected to be valid JSON objects
-            if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
-            {
-                _logger.LogDebug("Received web socket message that is not a json structure: {Message}", message);
-                return;
-            }
-
-            try
-            {
-                var info = _jsonSerializer.DeserializeFromString<WebSocketMessage<object>>(message);
-
-                _logger.LogDebug("Websocket message received: {0}", info.MessageType);
-
-                var tasks = _webSocketHandlers.Select(handler => Task.Run(() =>
-                {
-                    try
-                    {
-                        handler.ProcessMessage(info, taskCompletionSource).ConfigureAwait(false);
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogError(ex, "{HandlerType} failed processing WebSocket message {MessageType}",
-                            handler.GetType().Name, info.MessageType ?? string.Empty);
-                    }
-                }));
-
-                await Task.WhenAll(tasks);
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error processing web socket message");
-            }
-        }
-    }
-}

+ 0 - 1
Jellyfin.Server/Startup.cs

@@ -64,7 +64,6 @@ namespace Jellyfin.Server
             app.UseResponseCompression();
 
             // TODO app.UseMiddleware<WebSocketMiddleware>();
-            app.Use(serverApplicationHost.ExecuteWebsocketHandlerAsync);
 
             // TODO use when old API is removed: app.UseAuthentication();
             app.UseJellyfinApiSwagger();

+ 22 - 21
MediaBrowser.Api/Sessions/SessionInfoWebSocketListener.cs

@@ -31,46 +31,46 @@ namespace MediaBrowser.Api.Sessions
         {
             _sessionManager = sessionManager;
 
-            _sessionManager.SessionStarted += _sessionManager_SessionStarted;
-            _sessionManager.SessionEnded += _sessionManager_SessionEnded;
-            _sessionManager.PlaybackStart += _sessionManager_PlaybackStart;
-            _sessionManager.PlaybackStopped += _sessionManager_PlaybackStopped;
-            _sessionManager.PlaybackProgress += _sessionManager_PlaybackProgress;
-            _sessionManager.CapabilitiesChanged += _sessionManager_CapabilitiesChanged;
-            _sessionManager.SessionActivity += _sessionManager_SessionActivity;
+            _sessionManager.SessionStarted += OnSessionManagerSessionStarted;
+            _sessionManager.SessionEnded += OnSessionManagerSessionEnded;
+            _sessionManager.PlaybackStart += OnSessionManagerPlaybackStart;
+            _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped;
+            _sessionManager.PlaybackProgress += OnSessionManagerPlaybackProgress;
+            _sessionManager.CapabilitiesChanged += OnSessionManagerCapabilitiesChanged;
+            _sessionManager.SessionActivity += OnSessionManagerSessionActivity;
         }
 
-        void _sessionManager_SessionActivity(object sender, SessionEventArgs e)
+        private void OnSessionManagerSessionActivity(object sender, SessionEventArgs e)
         {
             SendData(false);
         }
 
-        void _sessionManager_CapabilitiesChanged(object sender, SessionEventArgs e)
+        private void OnSessionManagerCapabilitiesChanged(object sender, SessionEventArgs e)
         {
             SendData(true);
         }
 
-        void _sessionManager_PlaybackProgress(object sender, PlaybackProgressEventArgs e)
+        private void OnSessionManagerPlaybackProgress(object sender, PlaybackProgressEventArgs e)
         {
             SendData(!e.IsAutomated);
         }
 
-        void _sessionManager_PlaybackStopped(object sender, PlaybackStopEventArgs e)
+        private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e)
         {
             SendData(true);
         }
 
-        void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e)
+        private void OnSessionManagerPlaybackStart(object sender, PlaybackProgressEventArgs e)
         {
             SendData(true);
         }
 
-        void _sessionManager_SessionEnded(object sender, SessionEventArgs e)
+        private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
         {
             SendData(true);
         }
 
-        void _sessionManager_SessionStarted(object sender, SessionEventArgs e)
+        private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e)
         {
             SendData(true);
         }
@@ -84,15 +84,16 @@ namespace MediaBrowser.Api.Sessions
             return Task.FromResult(_sessionManager.Sessions);
         }
 
+        /// <inheritdoc />
         protected override void Dispose(bool dispose)
         {
-            _sessionManager.SessionStarted -= _sessionManager_SessionStarted;
-            _sessionManager.SessionEnded -= _sessionManager_SessionEnded;
-            _sessionManager.PlaybackStart -= _sessionManager_PlaybackStart;
-            _sessionManager.PlaybackStopped -= _sessionManager_PlaybackStopped;
-            _sessionManager.PlaybackProgress -= _sessionManager_PlaybackProgress;
-            _sessionManager.CapabilitiesChanged -= _sessionManager_CapabilitiesChanged;
-            _sessionManager.SessionActivity -= _sessionManager_SessionActivity;
+            _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
+            _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
+            _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
+            _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
+            _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
+            _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
+            _sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
 
             base.Dispose(dispose);
         }

+ 8 - 8
MediaBrowser.Api/System/ActivityLogWebSocketListener.cs

@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System;
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Activity;
@@ -10,7 +10,7 @@ namespace MediaBrowser.Api.System
     /// <summary>
     /// Class SessionInfoWebSocketListener
     /// </summary>
-    public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<List<ActivityLogEntry>, WebSocketListenerState>
+    public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState>
     {
         /// <summary>
         /// Gets the name.
@@ -26,10 +26,10 @@ namespace MediaBrowser.Api.System
         public ActivityLogWebSocketListener(ILogger<ActivityLogWebSocketListener> logger, IActivityManager activityManager) : base(logger)
         {
             _activityManager = activityManager;
-            _activityManager.EntryCreated += _activityManager_EntryCreated;
+            _activityManager.EntryCreated += OnEntryCreated;
         }
 
-        void _activityManager_EntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
+        private void OnEntryCreated(object sender, GenericEventArgs<ActivityLogEntry> e)
         {
             SendData(true);
         }
@@ -38,15 +38,15 @@ namespace MediaBrowser.Api.System
         /// Gets the data to send.
         /// </summary>
         /// <returns>Task{SystemInfo}.</returns>
-        protected override Task<List<ActivityLogEntry>> GetDataToSend()
+        protected override Task<ActivityLogEntry[]> GetDataToSend()
         {
-            return Task.FromResult(new List<ActivityLogEntry>());
+            return Task.FromResult(Array.Empty<ActivityLogEntry>());
         }
 
-
+        /// <inheritdoc />
         protected override void Dispose(bool dispose)
         {
-            _activityManager.EntryCreated -= _activityManager_EntryCreated;
+            _activityManager.EntryCreated -= OnEntryCreated;
 
             base.Dispose(dispose);
         }

+ 0 - 2
MediaBrowser.Controller/IServerApplicationHost.cs

@@ -116,7 +116,5 @@ namespace MediaBrowser.Controller
         string ReverseVirtualPath(string path);
 
         Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next);
-
-        Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next);
     }
 }

+ 5 - 7
MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs

@@ -77,8 +77,6 @@ namespace MediaBrowser.Controller.Net
             return Task.CompletedTask;
         }
 
-        protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
         /// <summary>
         /// Starts sending messages over a web socket
         /// </summary>
@@ -87,12 +85,12 @@ namespace MediaBrowser.Controller.Net
         {
             var vals = message.Data.Split(',');
 
-            var dueTimeMs = long.Parse(vals[0], UsCulture);
-            var periodMs = long.Parse(vals[1], UsCulture);
+            var dueTimeMs = long.Parse(vals[0], CultureInfo.InvariantCulture);
+            var periodMs = long.Parse(vals[1], CultureInfo.InvariantCulture);
 
             var cancellationTokenSource = new CancellationTokenSource();
 
-            Logger.LogDebug("{1} Begin transmitting over websocket to {0}", message.Connection.RemoteEndPoint, GetType().Name);
+            Logger.LogDebug("WS {1} begin transmitting to {0}", message.Connection.RemoteEndPoint, GetType().Name);
 
             var state = new TStateType
             {
@@ -154,7 +152,6 @@ namespace MediaBrowser.Controller.Net
                     {
                         MessageType = Name,
                         Data = data
-
                     }, cancellationToken).ConfigureAwait(false);
 
                     state.DateLastSendUtc = DateTime.UtcNow;
@@ -197,7 +194,7 @@ namespace MediaBrowser.Controller.Net
         /// <param name="connection">The connection.</param>
         private void DisposeConnection(Tuple<IWebSocketConnection, CancellationTokenSource, TStateType> connection)
         {
-            Logger.LogDebug("{1} stop transmitting over websocket to {0}", connection.Item1.RemoteEndPoint, GetType().Name);
+            Logger.LogDebug("WS {1} stop transmitting to {0}", connection.Item1.RemoteEndPoint, GetType().Name);
 
             // TODO disposing the connection seems to break websockets in subtle ways, so what is the purpose of this function really...
             // connection.Item1.Dispose();
@@ -242,6 +239,7 @@ namespace MediaBrowser.Controller.Net
         public void Dispose()
         {
             Dispose(true);
+            GC.SuppressFinalize(this);
         }
     }
 

+ 4 - 23
MediaBrowser.Controller/Net/IHttpServer.cs

@@ -1,17 +1,15 @@
 using System;
 using System.Collections.Generic;
-using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Services;
 using Microsoft.AspNetCore.Http;
 
 namespace MediaBrowser.Controller.Net
 {
     /// <summary>
-    /// Interface IHttpServer
+    /// Interface IHttpServer.
     /// </summary>
-    public interface IHttpServer : IDisposable
+    public interface IHttpServer
     {
         /// <summary>
         /// Gets the URL prefix.
@@ -19,11 +17,6 @@ namespace MediaBrowser.Controller.Net
         /// <value>The URL prefix.</value>
         string[] UrlPrefixes { get; }
 
-        /// <summary>
-        /// Stops this instance.
-        /// </summary>
-        void Stop();
-
         /// <summary>
         /// Occurs when [web socket connected].
         /// </summary>
@@ -39,23 +32,11 @@ namespace MediaBrowser.Controller.Net
         /// </summary>
         string GlobalResponse { get; set; }
 
-        /// <summary>
-        /// Sends the http context to the socket listener
-        /// </summary>
-        /// <param name="ctx"></param>
-        /// <returns></returns>
-        Task ProcessWebSocketRequest(HttpContext ctx);
-
         /// <summary>
         /// The HTTP request handler
         /// </summary>
-        /// <param name="httpReq"></param>
-        /// <param name="urlString"></param>
-        /// <param name="host"></param>
-        /// <param name="localPath"></param>
-        /// <param name="cancellationToken"></param>
+        /// <param name="context"></param>
         /// <returns></returns>
-        Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath,
-            CancellationToken cancellationToken);
+        Task RequestHandler(HttpContext context);
     }
 }

+ 9 - 32
MediaBrowser.Controller/Net/IWebSocketConnection.cs

@@ -1,4 +1,7 @@
+#nullable enable
+
 using System;
+using System.Net;
 using System.Net.WebSockets;
 using System.Threading;
 using System.Threading.Tasks;
@@ -7,18 +10,12 @@ using Microsoft.AspNetCore.Http;
 
 namespace MediaBrowser.Controller.Net
 {
-    public interface IWebSocketConnection : IDisposable
+    public interface IWebSocketConnection
     {
         /// <summary>
         /// Occurs when [closed].
         /// </summary>
-        event EventHandler<EventArgs> Closed;
-
-        /// <summary>
-        /// Gets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        Guid Id { get; }
+        event EventHandler<EventArgs>? Closed;
 
         /// <summary>
         /// Gets the last activity date.
@@ -26,22 +23,17 @@ namespace MediaBrowser.Controller.Net
         /// <value>The last activity date.</value>
         DateTime LastActivityDate { get; }
 
-        /// <summary>
-        /// Gets or sets the URL.
-        /// </summary>
-        /// <value>The URL.</value>
-        string Url { get; set; }
         /// <summary>
         /// Gets or sets the query string.
         /// </summary>
         /// <value>The query string.</value>
-        IQueryCollection QueryString { get; set; }
+        IQueryCollection QueryString { get; }
 
         /// <summary>
         /// Gets or sets the receive action.
         /// </summary>
         /// <value>The receive action.</value>
-        Func<WebSocketMessageInfo, Task> OnReceive { get; set; }
+        Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
 
         /// <summary>
         /// Gets the state.
@@ -53,7 +45,7 @@ namespace MediaBrowser.Controller.Net
         /// Gets the remote end point.
         /// </summary>
         /// <value>The remote end point.</value>
-        string RemoteEndPoint { get; }
+        IPAddress? RemoteEndPoint { get; }
 
         /// <summary>
         /// Sends a message asynchronously.
@@ -65,21 +57,6 @@ namespace MediaBrowser.Controller.Net
         /// <exception cref="ArgumentNullException">message</exception>
         Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken);
 
-        /// <summary>
-        /// Sends a message asynchronously.
-        /// </summary>
-        /// <param name="buffer">The buffer.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        Task SendAsync(byte[] buffer, CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Sends a message asynchronously.
-        /// </summary>
-        /// <param name="text">The text.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        /// <exception cref="ArgumentNullException">buffer</exception>
-        Task SendAsync(string text, CancellationToken cancellationToken);
+        Task ProcessAsync(CancellationToken cancellationToken = default);
     }
 }

+ 2 - 1
MediaBrowser.Controller/Session/ISessionController.cs

@@ -1,3 +1,4 @@
+using System;
 using System.Threading;
 using System.Threading.Tasks;
 
@@ -20,6 +21,6 @@ namespace MediaBrowser.Controller.Session
         /// <summary>
         /// Sends the message.
         /// </summary>
-        Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken);
+        Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken);
     }
 }

+ 24 - 50
MediaBrowser.Controller/Session/SessionInfo.cs

@@ -10,13 +10,23 @@ using Microsoft.Extensions.Logging;
 namespace MediaBrowser.Controller.Session
 {
     /// <summary>
-    /// Class SessionInfo
+    /// Class SessionInfo.
     /// </summary>
-    public class SessionInfo : IDisposable
+    public sealed class SessionInfo : IDisposable
     {
-        private ISessionManager _sessionManager;
+        // 1 second
+        private const long ProgressIncrement = 10000000;
+
+        private readonly ISessionManager _sessionManager;
         private readonly ILogger _logger;
 
+
+        private readonly object _progressLock = new object();
+        private Timer _progressTimer;
+        private PlaybackProgressInfo _lastProgressInfo;
+
+        private bool _disposed = false;
+
         public SessionInfo(ISessionManager sessionManager, ILogger logger)
         {
             _sessionManager = sessionManager;
@@ -97,8 +107,6 @@ namespace MediaBrowser.Controller.Session
         /// <value>The name of the device.</value>
         public string DeviceName { get; set; }
 
-        public string DeviceType { get; set; }
-
         /// <summary>
         /// Gets or sets the now playing item.
         /// </summary>
@@ -128,22 +136,6 @@ namespace MediaBrowser.Controller.Session
         [JsonIgnore]
         public ISessionController[] SessionControllers { get; set; }
 
-        /// <summary>
-        /// Gets or sets the supported commands.
-        /// </summary>
-        /// <value>The supported commands.</value>
-        public string[] SupportedCommands
-        {
-            get
-            {
-                if (Capabilities == null)
-                {
-                    return new string[] { };
-                }
-                return Capabilities.SupportedCommands;
-            }
-        }
-
         public TranscodingInfo TranscodingInfo { get; set; }
 
         /// <summary>
@@ -215,6 +207,14 @@ namespace MediaBrowser.Controller.Session
             }
         }
 
+        public QueueItem[] NowPlayingQueue { get; set; }
+
+        public bool HasCustomDeviceName { get; set; }
+
+        public string PlaylistItemId { get; set; }
+
+        public string UserPrimaryImageTag { get; set; }
+
         public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
         {
             var controllers = SessionControllers.ToList();
@@ -258,10 +258,6 @@ namespace MediaBrowser.Controller.Session
             return false;
         }
 
-        private readonly object _progressLock = new object();
-        private Timer _progressTimer;
-        private PlaybackProgressInfo _lastProgressInfo;
-
         public void StartAutomaticProgress(PlaybackProgressInfo progressInfo)
         {
             if (_disposed)
@@ -284,9 +280,6 @@ namespace MediaBrowser.Controller.Session
             }
         }
 
-        // 1 second
-        private const long ProgressIncrement = 10000000;
-
         private async void OnProgressTimerCallback(object state)
         {
             if (_disposed)
@@ -345,8 +338,7 @@ namespace MediaBrowser.Controller.Session
             }
         }
 
-        private bool _disposed = false;
-
+        /// <inheritdoc />
         public void Dispose()
         {
             _disposed = true;
@@ -358,30 +350,12 @@ namespace MediaBrowser.Controller.Session
 
             foreach (var controller in controllers)
             {
-                var disposable = controller as IDisposable;
-
-                if (disposable != null)
+                if (controller is IDisposable disposable)
                 {
                     _logger.LogDebug("Disposing session controller {0}", disposable.GetType().Name);
-
-                    try
-                    {
-                        disposable.Dispose();
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogError(ex, "Error disposing session controller");
-                    }
+                    disposable.Dispose();
                 }
             }
-
-            _sessionManager = null;
         }
-
-        public QueueItem[] NowPlayingQueue { get; set; }
-        public bool HasCustomDeviceName { get; set; }
-        public string PlaylistItemId { get; set; }
-        public string ServerId { get; set; }
-        public string UserPrimaryImageTag { get; set; }
     }
 }

+ 6 - 2
MediaBrowser.Model/Net/WebSocketMessage.cs

@@ -1,5 +1,8 @@
+
 #pragma warning disable CS1591
 
+using System;
+
 namespace MediaBrowser.Model.Net
 {
     /// <summary>
@@ -13,7 +16,9 @@ namespace MediaBrowser.Model.Net
         /// </summary>
         /// <value>The type of the message.</value>
         public string MessageType { get; set; }
-        public string MessageId { get; set; }
+
+        public Guid MessageId { get; set; }
+
         public string ServerId { get; set; }
 
         /// <summary>
@@ -22,5 +27,4 @@ namespace MediaBrowser.Model.Net
         /// <value>The data.</value>
         public T Data { get; set; }
     }
-
 }