aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/Session
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations/Session')
-rw-r--r--Emby.Server.Implementations/Session/HttpSessionController.cs191
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs948
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs312
-rw-r--r--Emby.Server.Implementations/Session/WebSocketController.cs89
4 files changed, 797 insertions, 743 deletions
diff --git a/Emby.Server.Implementations/Session/HttpSessionController.cs b/Emby.Server.Implementations/Session/HttpSessionController.cs
deleted file mode 100644
index 9281f82b3..000000000
--- a/Emby.Server.Implementations/Session/HttpSessionController.cs
+++ /dev/null
@@ -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")).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 = new 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;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index fa0ab62d3..341d2c8e6 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1,3 +1,7 @@
+#nullable disable
+
+#pragma warning disable CS1591
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -5,125 +9,139 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using Jellyfin.Data.Entities.Security;
+using Jellyfin.Data.Enums;
+using Jellyfin.Data.Events;
+using Jellyfin.Data.Queries;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Session;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
-using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session;
+using MediaBrowser.Model.SyncPlay;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
+using Episode = MediaBrowser.Controller.Entities.TV.Episode;
namespace Emby.Server.Implementations.Session
{
/// <summary>
- /// Class SessionManager
+ /// Class SessionManager.
/// </summary>
public class SessionManager : ISessionManager, IDisposable
{
- /// <summary>
- /// The _user data repository
- /// </summary>
private readonly IUserDataManager _userDataManager;
-
- /// <summary>
- /// The _logger
- /// </summary>
- private readonly ILogger _logger;
-
+ private readonly ILogger<SessionManager> _logger;
+ private readonly IEventManager _eventManager;
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IMusicManager _musicManager;
private readonly IDtoService _dtoService;
private readonly IImageProcessor _imageProcessor;
private readonly IMediaSourceManager _mediaSourceManager;
-
- private readonly IHttpClient _httpClient;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IServerApplicationHost _appHost;
-
- private readonly IAuthenticationRepository _authRepo;
private readonly IDeviceManager _deviceManager;
/// <summary>
- /// The _active connections
+ /// The active connections.
/// </summary>
- private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections =
- new ConcurrentDictionary<string, SessionInfo>(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new (StringComparer.OrdinalIgnoreCase);
- public event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed;
-
- public event EventHandler<GenericEventArgs<AuthenticationResult>> AuthenticationSucceeded;
-
- /// <summary>
- /// Occurs when [playback start].
- /// </summary>
- public event EventHandler<PlaybackProgressEventArgs> PlaybackStart;
- /// <summary>
- /// Occurs when [playback progress].
- /// </summary>
- public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
- /// <summary>
- /// Occurs when [playback stopped].
- /// </summary>
- public event EventHandler<PlaybackStopEventArgs> PlaybackStopped;
+ private Timer _idleTimer;
- public event EventHandler<SessionEventArgs> SessionStarted;
- public event EventHandler<SessionEventArgs> CapabilitiesChanged;
- public event EventHandler<SessionEventArgs> SessionEnded;
- public event EventHandler<SessionEventArgs> SessionActivity;
+ private DtoOptions _itemInfoDtoOptions;
+ private bool _disposed = false;
public SessionManager(
+ ILogger<SessionManager> logger,
+ IEventManager eventManager,
IUserDataManager userDataManager,
- ILoggerFactory loggerFactory,
ILibraryManager libraryManager,
IUserManager userManager,
IMusicManager musicManager,
IDtoService dtoService,
IImageProcessor imageProcessor,
- IJsonSerializer jsonSerializer,
IServerApplicationHost appHost,
- IHttpClient httpClient,
- IAuthenticationRepository authRepo,
IDeviceManager deviceManager,
IMediaSourceManager mediaSourceManager)
{
+ _logger = logger;
+ _eventManager = eventManager;
_userDataManager = userDataManager;
- _logger = loggerFactory.CreateLogger(nameof(SessionManager));
_libraryManager = libraryManager;
_userManager = userManager;
_musicManager = musicManager;
_dtoService = dtoService;
_imageProcessor = imageProcessor;
- _jsonSerializer = jsonSerializer;
_appHost = appHost;
- _httpClient = httpClient;
- _authRepo = authRepo;
_deviceManager = deviceManager;
_mediaSourceManager = mediaSourceManager;
- _deviceManager.DeviceOptionsUpdated += _deviceManager_DeviceOptionsUpdated;
+
+ _deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
}
- private void _deviceManager_DeviceOptionsUpdated(object sender, GenericEventArgs<Tuple<string, DeviceOptions>> e)
+ /// <inheritdoc />
+ public event EventHandler<GenericEventArgs<AuthenticationRequest>> AuthenticationFailed;
+
+ /// <inheritdoc />
+ public event EventHandler<GenericEventArgs<AuthenticationResult>> AuthenticationSucceeded;
+
+ /// <summary>
+ /// Occurs when playback has started.
+ /// </summary>
+ public event EventHandler<PlaybackProgressEventArgs> PlaybackStart;
+
+ /// <summary>
+ /// Occurs when playback has progressed.
+ /// </summary>
+ public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
+
+ /// <summary>
+ /// Occurs when playback has stopped.
+ /// </summary>
+ public event EventHandler<PlaybackStopEventArgs> PlaybackStopped;
+
+ /// <inheritdoc />
+ public event EventHandler<SessionEventArgs> SessionStarted;
+
+ /// <inheritdoc />
+ public event EventHandler<SessionEventArgs> CapabilitiesChanged;
+
+ /// <inheritdoc />
+ public event EventHandler<SessionEventArgs> SessionEnded;
+
+ /// <inheritdoc />
+ public event EventHandler<SessionEventArgs> SessionActivity;
+
+ /// <inheritdoc />
+ public event EventHandler<SessionEventArgs> SessionControllerConnected;
+
+ /// <summary>
+ /// Gets all connections.
+ /// </summary>
+ /// <value>All connections.</value>
+ public IEnumerable<SessionInfo> Sessions => _activeConnections.Values.OrderByDescending(c => c.LastActivityDate);
+
+ private void OnDeviceManagerDeviceOptionsUpdated(object sender, GenericEventArgs<Tuple<string, DeviceOptions>> e)
{
foreach (var session in Sessions)
{
- if (string.Equals(session.DeviceId, e.Argument.Item1))
+ if (string.Equals(session.DeviceId, e.Argument.Item1, StringComparison.Ordinal))
{
if (!string.IsNullOrWhiteSpace(e.Argument.Item2.CustomName))
{
@@ -138,14 +156,37 @@ namespace Emby.Server.Implementations.Session
}
}
- private bool _disposed;
+ /// <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;
+ }
+
+ if (disposing)
+ {
+ _idleTimer?.Dispose();
+ }
+
+ _idleTimer = null;
+
+ _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
+
_disposed = true;
- _deviceManager.DeviceOptionsUpdated -= _deviceManager_DeviceOptionsUpdated;
}
- public void CheckDisposed()
+ private void CheckDisposed()
{
if (_disposed)
{
@@ -153,17 +194,11 @@ namespace Emby.Server.Implementations.Session
}
}
- /// <summary>
- /// Gets all connections.
- /// </summary>
- /// <value>All connections.</value>
- public IEnumerable<SessionInfo> Sessions => _activeConnections.Values.OrderByDescending(c => c.LastActivityDate).ToList();
-
private void OnSessionStarted(SessionInfo info)
{
if (!string.IsNullOrEmpty(info.DeviceId))
{
- var capabilities = GetSavedCapabilities(info.DeviceId);
+ var capabilities = _deviceManager.GetCapabilities(info.DeviceId);
if (capabilities != null)
{
@@ -171,33 +206,41 @@ namespace Emby.Server.Implementations.Session
}
}
- EventHelper.QueueEventIfNotNull(SessionStarted, this, new SessionEventArgs
- {
- SessionInfo = info
+ _eventManager.Publish(new SessionStartedEventArgs(info));
- }, _logger);
+ EventHelper.QueueEventIfNotNull(
+ SessionStarted,
+ this,
+ new SessionEventArgs
+ {
+ SessionInfo = info
+ },
+ _logger);
}
private void OnSessionEnded(SessionInfo info)
{
- EventHelper.QueueEventIfNotNull(SessionEnded, this, new SessionEventArgs
- {
- SessionInfo = info
+ EventHelper.QueueEventIfNotNull(
+ SessionEnded,
+ this,
+ new SessionEventArgs
+ {
+ SessionInfo = info
+ },
+ _logger);
- }, _logger);
+ _eventManager.Publish(new SessionEndedEventArgs(info));
info.Dispose();
}
- public void UpdateDeviceName(string sessionId, string deviceName)
+ /// <inheritdoc />
+ public void UpdateDeviceName(string sessionId, string reportedDeviceName)
{
var session = GetSession(sessionId);
-
- var key = GetSessionKey(session.Client, session.DeviceId);
-
if (session != null)
{
- session.DeviceName = deviceName;
+ session.DeviceName = reportedDeviceName;
}
}
@@ -210,10 +253,9 @@ namespace Emby.Server.Implementations.Session
/// <param name="deviceName">Name of the device.</param>
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
- /// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">user</exception>
- /// <exception cref="UnauthorizedAccessException"></exception>
- public SessionInfo LogSessionActivity(string appName,
+ /// <returns>SessionInfo.</returns>
+ public async Task<SessionInfo> LogSessionActivity(
+ string appName,
string appVersion,
string deviceId,
string deviceName,
@@ -226,61 +268,80 @@ namespace Emby.Server.Implementations.Session
{
throw new ArgumentNullException(nameof(appName));
}
+
if (string.IsNullOrEmpty(appVersion))
{
throw new ArgumentNullException(nameof(appVersion));
}
+
if (string.IsNullOrEmpty(deviceId))
{
throw new ArgumentNullException(nameof(deviceId));
}
var activityDate = DateTime.UtcNow;
- var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
+ var session = await GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
var lastActivityDate = session.LastActivityDate;
session.LastActivityDate = activityDate;
if (user != null)
{
var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue;
- user.LastActivityDate = activityDate;
if ((activityDate - userLastActivityDate).TotalSeconds > 60)
{
try
{
- _userManager.UpdateUser(user);
+ user.LastActivityDate = activityDate;
+ await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
}
- catch (Exception ex)
+ catch (DbUpdateConcurrencyException e)
{
- _logger.LogError("Error updating user", ex);
+ _logger.LogDebug(e, "Error updating user's last activity date.");
}
}
}
if ((activityDate - lastActivityDate).TotalSeconds > 10)
{
- SessionActivity?.Invoke(this, new SessionEventArgs
- {
- SessionInfo = session
- });
+ SessionActivity?.Invoke(
+ this,
+ new SessionEventArgs
+ {
+ SessionInfo = session
+ });
}
return session;
}
+ /// <inheritdoc />
+ public void OnSessionControllerConnected(SessionInfo session)
+ {
+ EventHelper.QueueEventIfNotNull(
+ SessionControllerConnected,
+ this,
+ new SessionEventArgs
+ {
+ SessionInfo = session
+ },
+ _logger);
+ }
+
+ /// <inheritdoc />
public void CloseIfNeeded(SessionInfo session)
{
if (!session.SessionControllers.Any(i => i.IsSessionActive))
{
var key = GetSessionKey(session.Client, session.DeviceId);
- _activeConnections.TryRemove(key, out var removed);
+ _activeConnections.TryRemove(key, out _);
OnSessionEnded(session);
}
}
+ /// <inheritdoc />
public void ReportSessionEnded(string sessionId)
{
CheckDisposed();
@@ -290,7 +351,7 @@ namespace Emby.Server.Implementations.Session
{
var key = GetSessionKey(session.Client, session.DeviceId);
- _activeConnections.TryRemove(key, out var removed);
+ _activeConnections.TryRemove(key, out _);
OnSessionEnded(session);
}
@@ -304,11 +365,12 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Updates the now playing item id.
/// </summary>
+ /// <returns>Task.</returns>
private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bool updateLastCheckInTime)
{
if (string.IsNullOrEmpty(info.MediaSourceId))
{
- info.MediaSourceId = info.ItemId.ToString("N");
+ info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
}
if (!info.ItemId.Equals(Guid.Empty) && info.Item == null && libraryItem != null)
@@ -320,8 +382,7 @@ namespace Emby.Server.Implementations.Session
var runtimeTicks = libraryItem.RunTimeTicks;
MediaSourceInfo mediaSource = null;
- var hasMediaSources = libraryItem as IHasMediaSources;
- if (hasMediaSources != null)
+ if (libraryItem is IHasMediaSources)
{
mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false);
@@ -373,7 +434,6 @@ namespace Emby.Server.Implementations.Session
/// Removes the now playing item id.
/// </summary>
/// <param name="session">The session.</param>
- /// <exception cref="ArgumentNullException">item</exception>
private void RemoveNowPlayingItem(SessionInfo session)
{
session.NowPlayingItem = null;
@@ -386,9 +446,7 @@ namespace Emby.Server.Implementations.Session
}
private static string GetSessionKey(string appName, string deviceId)
- {
- return appName + deviceId;
- }
+ => appName + deviceId;
/// <summary>
/// Gets the connection.
@@ -400,7 +458,13 @@ namespace Emby.Server.Implementations.Session
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
/// <returns>SessionInfo.</returns>
- private SessionInfo GetSessionInfo(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user)
+ private async Task<SessionInfo> GetSessionInfo(
+ string appName,
+ string appVersion,
+ string deviceId,
+ string deviceName,
+ string remoteEndPoint,
+ User user)
{
CheckDisposed();
@@ -408,18 +472,20 @@ namespace Emby.Server.Implementations.Session
{
throw new ArgumentNullException(nameof(deviceId));
}
+
var key = GetSessionKey(appName, deviceId);
CheckDisposed();
- var sessionInfo = _activeConnections.GetOrAdd(key, k =>
+ if (!_activeConnections.TryGetValue(key, out var sessionInfo))
{
- return CreateSession(k, appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
- });
+ _activeConnections[key] = await CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
+ sessionInfo = _activeConnections[key];
+ }
- sessionInfo.UserId = user == null ? Guid.Empty : user.Id;
- sessionInfo.UserName = user == null ? null : user.Name;
- sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary);
+ sessionInfo.UserId = user?.Id ?? Guid.Empty;
+ sessionInfo.UserName = user?.Username;
+ sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user);
sessionInfo.RemoteEndPoint = remoteEndPoint;
sessionInfo.Client = appName;
@@ -432,28 +498,35 @@ namespace Emby.Server.Implementations.Session
if (user == null)
{
- sessionInfo.AdditionalUsers = new SessionUserInfo[] { };
+ sessionInfo.AdditionalUsers = Array.Empty<SessionUserInfo>();
}
return sessionInfo;
}
- private SessionInfo CreateSession(string key, string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user)
+ private async Task<SessionInfo> CreateSession(
+ string key,
+ string appName,
+ string appVersion,
+ string deviceId,
+ string deviceName,
+ string remoteEndPoint,
+ User user)
{
var sessionInfo = new SessionInfo(this, _logger)
{
Client = appName,
DeviceId = deviceId,
ApplicationVersion = appVersion,
- Id = key.GetMD5().ToString("N"),
+ Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
ServerId = _appHost.SystemId
};
- var username = user == null ? null : user.Name;
+ var username = user?.Username;
- sessionInfo.UserId = user == null ? Guid.Empty : user.Id;
+ sessionInfo.UserId = user?.Id ?? Guid.Empty;
sessionInfo.UserName = username;
- sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary);
+ sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user);
sessionInfo.RemoteEndPoint = remoteEndPoint;
if (string.IsNullOrEmpty(deviceName))
@@ -461,7 +534,7 @@ namespace Emby.Server.Implementations.Session
deviceName = "Network Device";
}
- var deviceOptions = _deviceManager.GetDeviceOptions(deviceId);
+ var deviceOptions = await _deviceManager.GetDeviceOptions(deviceId).ConfigureAwait(false);
if (string.IsNullOrEmpty(deviceOptions.CustomName))
{
sessionInfo.DeviceName = deviceName;
@@ -480,7 +553,7 @@ namespace Emby.Server.Implementations.Session
{
var users = new List<User>();
- if (!session.UserId.Equals(Guid.Empty))
+ if (session.UserId != Guid.Empty)
{
var user = _userManager.GetUserById(session.UserId);
@@ -499,15 +572,11 @@ namespace Emby.Server.Implementations.Session
return users;
}
- private Timer _idleTimer;
-
private void StartIdleCheckTimer()
{
- if (_idleTimer == null)
- {
- _idleTimer = new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
- }
+ _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
+
private void StopIdleCheckTimer()
{
if (_idleTimer != null)
@@ -539,9 +608,9 @@ namespace Emby.Server.Implementations.Session
Item = session.NowPlayingItem,
ItemId = session.NowPlayingItem == null ? Guid.Empty : session.NowPlayingItem.Id,
SessionId = session.Id,
- MediaSourceId = session.PlayState == null ? null : session.PlayState.MediaSourceId,
- PositionTicks = session.PlayState == null ? null : session.PlayState.PositionTicks
- });
+ MediaSourceId = session.PlayState?.MediaSourceId,
+ PositionTicks = session.PlayState?.PositionTicks
+ }).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -575,11 +644,11 @@ namespace Emby.Server.Implementations.Session
}
/// <summary>
- /// Used to report that playback has started for an item
+ /// Used to report that playback has started for an item.
/// </summary>
/// <param name="info">The info.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">info</exception>
+ /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
public async Task OnPlaybackStart(PlaybackStartInfo info)
{
CheckDisposed();
@@ -591,7 +660,7 @@ namespace Emby.Server.Implementations.Session
var session = GetSession(info.SessionId);
- var libraryItem = info.ItemId.Equals(Guid.Empty)
+ var libraryItem = info.ItemId == Guid.Empty
? null
: GetNowPlayingItem(session, info.ItemId);
@@ -614,9 +683,7 @@ namespace Emby.Server.Implementations.Session
}
}
- // Nothing to save here
- // Fire events to inform plugins
- EventHelper.QueueEventIfNotNull(PlaybackStart, this, new PlaybackProgressEventArgs
+ var eventArgs = new PlaybackStartEventArgs
{
Item = libraryItem,
Users = users,
@@ -626,8 +693,17 @@ namespace Emby.Server.Implementations.Session
ClientName = session.Client,
DeviceId = session.DeviceId,
Session = session
+ };
- }, _logger);
+ await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
+
+ // Nothing to save here
+ // Fire events to inform plugins
+ EventHelper.QueueEventIfNotNull(
+ PlaybackStart,
+ this,
+ eventArgs,
+ _logger);
StartIdleCheckTimer();
}
@@ -644,12 +720,9 @@ namespace Emby.Server.Implementations.Session
data.PlayCount++;
data.LastPlayedDate = DateTime.UtcNow;
- if (item.SupportsPlayedStatus)
+ if (item.SupportsPlayedStatus && !item.SupportsPositionTicksResume)
{
- if (!(item is Video))
- {
- data.Played = true;
- }
+ data.Played = true;
}
else
{
@@ -659,14 +732,18 @@ namespace Emby.Server.Implementations.Session
_userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None);
}
+ /// <inheritdoc />
public Task OnPlaybackProgress(PlaybackProgressInfo info)
{
return OnPlaybackProgress(info, false);
}
/// <summary>
- /// Used to report playback progress for an item
+ /// Used to report playback progress for an item.
/// </summary>
+ /// <param name="info">The playback progress info.</param>
+ /// <param name="isAutomated">Whether this is an automated update.</param>
+ /// <returns>Task.</returns>
public async Task OnPlaybackProgress(PlaybackProgressInfo info, bool isAutomated)
{
CheckDisposed();
@@ -695,7 +772,7 @@ namespace Emby.Server.Implementations.Session
}
}
- PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs
+ var eventArgs = new PlaybackProgressEventArgs
{
Item = libraryItem,
Users = users,
@@ -709,7 +786,11 @@ namespace Emby.Server.Implementations.Session
PlaySessionId = info.PlaySessionId,
IsAutomated = isAutomated,
Session = session
- });
+ };
+
+ await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
+
+ PlaybackProgress?.Invoke(this, eventArgs);
if (!isAutomated)
{
@@ -743,14 +824,13 @@ namespace Emby.Server.Implementations.Session
{
_userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.None);
}
-
}
private static bool UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data)
{
var changed = false;
- if (user.Configuration.RememberAudioSelections)
+ if (user.RememberAudioSelections)
{
if (data.AudioStreamIndex != info.AudioStreamIndex)
{
@@ -767,7 +847,7 @@ namespace Emby.Server.Implementations.Session
}
}
- if (user.Configuration.RememberSubtitleSelections)
+ if (user.RememberSubtitleSelections)
{
if (data.SubtitleStreamIndex != info.SubtitleStreamIndex)
{
@@ -788,12 +868,12 @@ namespace Emby.Server.Implementations.Session
}
/// <summary>
- /// Used to report that playback has ended for an item
+ /// Used to report that playback has ended for an item.
/// </summary>
/// <param name="info">The info.</param>
/// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">info</exception>
- /// <exception cref="ArgumentOutOfRangeException">positionTicks</exception>
+ /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
+ /// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</exception>
public async Task OnPlaybackStopped(PlaybackStopInfo info)
{
CheckDisposed();
@@ -819,7 +899,7 @@ namespace Emby.Server.Implementations.Session
// Normalize
if (string.IsNullOrEmpty(info.MediaSourceId))
{
- info.MediaSourceId = info.ItemId.ToString("N");
+ info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
}
if (!info.ItemId.Equals(Guid.Empty) && info.Item == null && libraryItem != null)
@@ -830,8 +910,7 @@ namespace Emby.Server.Implementations.Session
{
MediaSourceInfo mediaSource = null;
- var hasMediaSources = libraryItem as IHasMediaSources;
- if (hasMediaSources != null)
+ if (libraryItem is IHasMediaSources)
{
mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false);
}
@@ -848,7 +927,8 @@ namespace Emby.Server.Implementations.Session
{
var msString = info.PositionTicks.HasValue ? (info.PositionTicks.Value / 10000).ToString(CultureInfo.InvariantCulture) : "unknown";
- _logger.LogInformation("Playback stopped reported by app {0} {1} playing {2}. Stopped at {3} ms",
+ _logger.LogInformation(
+ "Playback stopped reported by app {0} {1} playing {2}. Stopped at {3} ms",
session.Client,
session.ApplicationVersion,
info.Item.Name,
@@ -887,7 +967,7 @@ namespace Emby.Server.Implementations.Session
}
}
- EventHelper.QueueEventIfNotNull(PlaybackStopped, this, new PlaybackStopEventArgs
+ var eventArgs = new PlaybackStopEventArgs
{
Item = libraryItem,
Users = users,
@@ -899,8 +979,11 @@ namespace Emby.Server.Implementations.Session
ClientName = session.Client,
DeviceId = session.DeviceId,
Session = session
+ };
+
+ await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
- }, _logger);
+ EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger);
}
private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
@@ -936,14 +1019,17 @@ namespace Emby.Server.Implementations.Session
/// <param name="sessionId">The session identifier.</param>
/// <param name="throwOnMissing">if set to <c>true</c> [throw on missing].</param>
/// <returns>SessionInfo.</returns>
- /// <exception cref="ResourceNotFoundException"></exception>
+ /// <exception cref="ResourceNotFoundException">
+ /// No session with an Id equal to <c>sessionId</c> was found
+ /// and <c>throwOnMissing</c> is <c>true</c>.
+ /// </exception>
private SessionInfo GetSession(string sessionId, bool throwOnMissing = true)
{
- var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId));
-
+ var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
if (session == null && throwOnMissing)
{
- throw new ResourceNotFoundException(string.Format("Session {0} not found.", sessionId));
+ throw new ResourceNotFoundException(
+ string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
}
return session;
@@ -952,23 +1038,25 @@ namespace Emby.Server.Implementations.Session
private SessionInfo GetSessionToRemoteControl(string sessionId)
{
// Accept either device id or session id
- var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId));
+ var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId, StringComparison.Ordinal));
if (session == null)
{
- throw new ResourceNotFoundException(string.Format("Session {0} not found.", sessionId));
+ throw new ResourceNotFoundException(
+ string.Format(CultureInfo.InvariantCulture, "Session {0} not found.", sessionId));
}
return session;
}
+ /// <inheritdoc />
public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken)
{
CheckDisposed();
var generalCommand = new GeneralCommand
{
- Name = GeneralCommandType.DisplayMessage.ToString()
+ Name = GeneralCommandType.DisplayMessage
};
generalCommand.Arguments["Header"] = command.Header;
@@ -982,6 +1070,7 @@ namespace Emby.Server.Implementations.Session
return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
}
+ /// <inheritdoc />
public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, CancellationToken cancellationToken)
{
CheckDisposed();
@@ -994,27 +1083,46 @@ namespace Emby.Server.Implementations.Session
AssertCanControl(session, controllingSession);
}
- return SendMessageToSession(session, "GeneralCommand", command, cancellationToken);
+ return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken);
}
- private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken)
+ private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, CancellationToken cancellationToken)
{
- var controllers = session.SessionControllers.ToArray();
- var messageId = Guid.NewGuid().ToString("N");
+ 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);
+ }
+ }
+
+ private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data, CancellationToken cancellationToken)
+ {
+ IEnumerable<Task> GetTasks()
+ {
+ 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, cancellationToken);
+ }
+ }
}
+
+ return Task.WhenAll(GetTasks());
}
+ /// <inheritdoc />
public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken)
{
CheckDisposed();
var session = GetSessionToRemoteControl(sessionId);
- var user = !session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(session.UserId) : null;
+ var user = session.UserId == Guid.Empty ? null : _userManager.GetUserById(session.UserId);
List<BaseItem> items;
@@ -1039,7 +1147,7 @@ namespace Emby.Server.Implementations.Session
if (command.PlayCommand == PlayCommand.PlayShuffle)
{
- items = items.OrderBy(i => Guid.NewGuid()).ToList();
+ items.Shuffle();
command.PlayCommand = PlayCommand.PlayNow;
}
@@ -1049,30 +1157,32 @@ namespace Emby.Server.Implementations.Session
{
if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
{
- throw new ArgumentException(string.Format("{0} is not allowed to play media.", user.Name));
+ throw new ArgumentException(
+ string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username));
}
}
- if (user != null && command.ItemIds.Length == 1 && user.Configuration.EnableNextEpisodeAutoPlay)
+ if (user != null
+ && command.ItemIds.Length == 1
+ && user.EnableNextEpisodeAutoPlay
+ && _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode)
{
- var episode = _libraryManager.GetItemById(command.ItemIds[0]) as Episode;
- if (episode != null)
+ var series = episode.Series;
+ if (series != null)
{
- var series = episode.Series;
- if (series != null)
+ var episodes = series.GetEpisodes(
+ user,
+ new DtoOptions(false)
+ {
+ EnableImages = false
+ })
+ .Where(i => !i.IsVirtualItem)
+ .SkipWhile(i => i.Id != episode.Id)
+ .ToList();
+
+ if (episodes.Count > 0)
{
- var episodes = series.GetEpisodes(user, new DtoOptions(false)
- {
- EnableImages = false
- })
- .Where(i => !i.IsVirtualItem)
- .SkipWhile(i => i.Id != episode.Id)
- .ToList();
-
- if (episodes.Count > 0)
- {
- command.ItemIds = episodes.Select(i => i.Id).ToArray();
- }
+ command.ItemIds = episodes.Select(i => i.Id).ToArray();
}
}
}
@@ -1087,22 +1197,36 @@ namespace Emby.Server.Implementations.Session
}
}
- await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
+ await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken)
+ {
+ CheckDisposed();
+ var session = GetSession(sessionId);
+ await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public async Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancellationToken)
+ {
+ CheckDisposed();
+ var session = GetSession(sessionId);
+ await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).ConfigureAwait(false);
}
- private IList<BaseItem> TranslateItemForPlayback(Guid id, User user)
+ private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
{
var item = _libraryManager.GetItemById(id);
if (item == null)
{
_logger.LogError("A non-existant item Id {0} was passed into TranslateItemForPlayback", id);
- return new List<BaseItem>();
+ return Array.Empty<BaseItem>();
}
- var byName = item as IItemByName;
-
- if (byName != null)
+ if (item is IItemByName byName)
{
return byName.GetTaggedItems(new InternalItemsQuery(user)
{
@@ -1111,13 +1235,13 @@ namespace Emby.Server.Implementations.Session
DtoOptions = new DtoOptions(false)
{
EnableImages = false,
- Fields = new ItemFields[]
+ Fields = new[]
{
ItemFields.SortName
}
},
IsVirtualItem = false,
- OrderBy = new ValueTuple<string, SortOrder>[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) }
+ OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
});
}
@@ -1138,12 +1262,11 @@ namespace Emby.Server.Implementations.Session
}
},
IsVirtualItem = false,
- OrderBy = new ValueTuple<string, SortOrder>[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) }
-
+ OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
});
}
- return new List<BaseItem> { item };
+ return new[] { item };
}
private IEnumerable<BaseItem> TranslateItemForInstantMix(Guid id, User user)
@@ -1152,27 +1275,31 @@ namespace Emby.Server.Implementations.Session
if (item == null)
{
- _logger.LogError("A non-existant item Id {0} was passed into TranslateItemForInstantMix", id);
+ _logger.LogError("A non-existent item Id {0} was passed into TranslateItemForInstantMix", id);
return new List<BaseItem>();
}
return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false });
}
+ /// <inheritdoc />
public Task SendBrowseCommand(string controllingSessionId, string sessionId, BrowseRequest command, CancellationToken cancellationToken)
{
var generalCommand = new GeneralCommand
{
- Name = GeneralCommandType.DisplayContent.ToString()
+ Name = GeneralCommandType.DisplayContent,
+ Arguments =
+ {
+ ["ItemId"] = command.ItemId,
+ ["ItemName"] = command.ItemName,
+ ["ItemType"] = command.ItemType
+ }
};
- generalCommand.Arguments["ItemId"] = command.ItemId;
- generalCommand.Arguments["ItemName"] = command.ItemName;
- generalCommand.Arguments["ItemType"] = command.ItemType;
-
return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
}
+ /// <inheritdoc />
public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, CancellationToken cancellationToken)
{
CheckDisposed();
@@ -1185,19 +1312,20 @@ namespace Emby.Server.Implementations.Session
AssertCanControl(session, controllingSession);
if (!controllingSession.UserId.Equals(Guid.Empty))
{
- command.ControllingUserId = controllingSession.UserId.ToString("N");
+ command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture);
}
}
- return SendMessageToSession(session, "Playstate", command, cancellationToken);
+ return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
}
- private void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
+ private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
{
if (session == null)
{
throw new ArgumentNullException(nameof(session));
}
+
if (controllingSession == null)
{
throw new ArgumentNullException(nameof(controllingSession));
@@ -1209,26 +1337,11 @@ namespace Emby.Server.Implementations.Session
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
- public async Task SendRestartRequiredNotification(CancellationToken cancellationToken)
+ public Task SendRestartRequiredNotification(CancellationToken cancellationToken)
{
CheckDisposed();
- var sessions = Sessions.ToList();
-
- var tasks = sessions.Select(session => Task.Run(async () =>
- {
- try
- {
- await SendMessageToSession(session, "RestartRequired", string.Empty, cancellationToken).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError("Error in SendRestartRequiredNotification.", ex);
- }
-
- }, cancellationToken)).ToArray();
-
- await Task.WhenAll(tasks).ConfigureAwait(false);
+ return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
}
/// <summary>
@@ -1240,22 +1353,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
- var sessions = Sessions.ToList();
-
- var tasks = sessions.Select(session => Task.Run(async () =>
- {
- try
- {
- await SendMessageToSession(session, "ServerShuttingDown", string.Empty, cancellationToken).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError("Error in SendServerShutdownNotification.", ex);
- }
-
- }, cancellationToken)).ToArray();
-
- return Task.WhenAll(tasks);
+ return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken);
}
/// <summary>
@@ -1269,22 +1367,7 @@ namespace Emby.Server.Implementations.Session
_logger.LogDebug("Beginning SendServerRestartNotification");
- var sessions = Sessions.ToList();
-
- var tasks = sessions.Select(session => Task.Run(async () =>
- {
- try
- {
- await SendMessageToSession(session, "ServerRestarting", string.Empty, cancellationToken).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError("Error in SendServerRestartNotification.", ex);
- }
-
- }, cancellationToken)).ToArray();
-
- return Task.WhenAll(tasks);
+ return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken);
}
/// <summary>
@@ -1300,12 +1383,12 @@ namespace Emby.Server.Implementations.Session
var session = GetSession(sessionId);
- if (session.UserId.Equals(userId))
+ if (session.UserId == userId)
{
throw new ArgumentException("The requested user is already the primary user of the session.");
}
- if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
+ if (session.AdditionalUsers.All(i => i.UserId != userId))
{
var user = _userManager.GetUserById(userId);
@@ -1314,7 +1397,7 @@ namespace Emby.Server.Implementations.Session
list.Add(new SessionUserInfo
{
UserId = userId,
- UserName = user.Name
+ UserName = user.Username
});
session.AdditionalUsers = list.ToArray();
@@ -1353,14 +1436,19 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Authenticates the new session.
/// </summary>
- /// <param name="request">The request.</param>
- /// <returns>Task{SessionInfo}.</returns>
+ /// <param name="request">The authenticationrequest.</param>
+ /// <returns>The authentication result.</returns>
public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
{
return AuthenticateNewSessionInternal(request, true);
}
- public Task<AuthenticationResult> CreateNewSession(AuthenticationRequest request)
+ /// <summary>
+ /// Directly authenticates the session without enforcing password.
+ /// </summary>
+ /// <param name="request">The authentication request.</param>
+ /// <returns>The authentication result.</returns>
+ public Task<AuthenticationResult> AuthenticateDirect(AuthenticationRequest request)
{
return AuthenticateNewSessionInternal(request, false);
}
@@ -1370,52 +1458,52 @@ namespace Emby.Server.Implementations.Session
CheckDisposed();
User user = null;
- if (!request.UserId.Equals(Guid.Empty))
+ if (request.UserId != Guid.Empty)
{
- user = _userManager.Users
- .FirstOrDefault(i => i.Id == request.UserId);
+ user = _userManager.GetUserById(request.UserId);
}
- if (user == null)
+ user ??= _userManager.GetUserByName(request.Username);
+
+ if (enforcePassword)
{
- user = _userManager.Users
- .FirstOrDefault(i => string.Equals(request.Username, i.Name, StringComparison.OrdinalIgnoreCase));
+ user = await _userManager.AuthenticateUser(
+ request.Username,
+ request.Password,
+ null,
+ request.RemoteEndPoint,
+ true).ConfigureAwait(false);
}
- if (user != null)
+ if (user == null)
{
- // TODO: Move this to userManager?
- if (!string.IsNullOrEmpty(request.DeviceId))
- {
- if (!_deviceManager.CanAccessDevice(user, request.DeviceId))
- {
- throw new SecurityException("User is not allowed access from this device.");
- }
- }
+ AuthenticationFailed?.Invoke(this, new GenericEventArgs<AuthenticationRequest>(request));
+ throw new AuthenticationException("Invalid username or password entered.");
}
- if (enforcePassword)
+ if (!string.IsNullOrEmpty(request.DeviceId)
+ && !_deviceManager.CanAccessDevice(user, request.DeviceId))
{
- var result = await _userManager.AuthenticateUser(request.Username, request.Password, request.PasswordSha1, request.RemoteEndPoint, true).ConfigureAwait(false);
-
- if (result == null)
- {
- AuthenticationFailed?.Invoke(this, new GenericEventArgs<AuthenticationRequest>(request));
-
- throw new SecurityException("Invalid user or password entered.");
- }
+ throw new SecurityException("User is not allowed access from this device.");
+ }
- user = result;
+ int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id));
+ int maxActiveSessions = user.MaxActiveSessions;
+ _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCount, maxActiveSessions);
+ if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions)
+ {
+ throw new SecurityException("User is at their maximum number of sessions.");
}
- var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName);
+ var token = await GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName).ConfigureAwait(false);
- var session = LogSessionActivity(request.App,
+ var session = await LogSessionActivity(
+ request.App,
request.AppVersion,
request.DeviceId,
request.DeviceName,
request.RemoteEndPoint,
- user);
+ user).ConfigureAwait(false);
var returnResult = new AuthenticationResult
{
@@ -1430,21 +1518,21 @@ namespace Emby.Server.Implementations.Session
return returnResult;
}
- private string GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
+ private async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
{
- var existing = _authRepo.Get(new AuthenticationInfoQuery
- {
- DeviceId = deviceId,
- UserId = user.Id,
- Limit = 1
-
- }).Items.FirstOrDefault();
-
- var allExistingForDevice = _authRepo.Get(new AuthenticationInfoQuery
- {
- DeviceId = deviceId
+ var existing = (await _deviceManager.GetDevices(
+ new DeviceQuery
+ {
+ DeviceId = deviceId,
+ UserId = user.Id,
+ Limit = 1
+ }).ConfigureAwait(false)).Items.FirstOrDefault();
- }).Items;
+ var allExistingForDevice = (await _deviceManager.GetDevices(
+ new DeviceQuery
+ {
+ DeviceId = deviceId
+ }).ConfigureAwait(false)).Items;
foreach (var auth in allExistingForDevice)
{
@@ -1452,43 +1540,29 @@ namespace Emby.Server.Implementations.Session
{
try
{
- Logout(auth);
+ await Logout(auth).ConfigureAwait(false);
}
- catch
+ catch (Exception ex)
{
-
+ _logger.LogError(ex, "Error while logging out.");
}
}
}
if (existing != null)
{
- _logger.LogInformation("Reissuing access token: " + existing.AccessToken);
+ _logger.LogInformation("Reissuing access token: {Token}", existing.AccessToken);
return existing.AccessToken;
}
- var now = DateTime.UtcNow;
-
- var newToken = new AuthenticationInfo
- {
- AppName = app,
- AppVersion = appVersion,
- DateCreated = now,
- DateLastActivity = now,
- DeviceId = deviceId,
- DeviceName = deviceName,
- UserId = user.Id,
- AccessToken = Guid.NewGuid().ToString("N"),
- UserName = user.Name
- };
-
_logger.LogInformation("Creating new access token for user {0}", user.Id);
- _authRepo.Create(newToken);
+ var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).ConfigureAwait(false);
- return newToken.AccessToken;
+ return device.AccessToken;
}
- public void Logout(string accessToken)
+ /// <inheritdoc />
+ public async Task Logout(string accessToken)
{
CheckDisposed();
@@ -1497,29 +1571,30 @@ namespace Emby.Server.Implementations.Session
throw new ArgumentNullException(nameof(accessToken));
}
- var existing = _authRepo.Get(new AuthenticationInfoQuery
- {
- Limit = 1,
- AccessToken = accessToken
-
- }).Items.FirstOrDefault();
+ var existing = (await _deviceManager.GetDevices(
+ new DeviceQuery
+ {
+ Limit = 1,
+ AccessToken = accessToken
+ }).ConfigureAwait(false)).Items;
- if (existing != null)
+ if (existing.Count > 0)
{
- Logout(existing);
+ await Logout(existing[0]).ConfigureAwait(false);
}
}
- public void Logout(AuthenticationInfo existing)
+ /// <inheritdoc />
+ public async Task Logout(Device device)
{
CheckDisposed();
- _logger.LogInformation("Logging out access token {0}", existing.AccessToken);
+ _logger.LogInformation("Logging out access token {0}", device.AccessToken);
- _authRepo.Delete(existing);
+ await _deviceManager.DeleteDevice(device).ConfigureAwait(false);
var sessions = Sessions
- .Where(i => string.Equals(i.DeviceId, existing.DeviceId, StringComparison.OrdinalIgnoreCase))
+ .Where(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var session in sessions)
@@ -1530,34 +1605,30 @@ namespace Emby.Server.Implementations.Session
}
catch (Exception ex)
{
- _logger.LogError("Error reporting session ended", ex);
+ _logger.LogError(ex, "Error reporting session ended");
}
}
}
- public void RevokeUserTokens(Guid userId, string currentAccessToken)
+ /// <inheritdoc />
+ public async Task RevokeUserTokens(Guid userId, string currentAccessToken)
{
CheckDisposed();
- var existing = _authRepo.Get(new AuthenticationInfoQuery
+ var existing = await _deviceManager.GetDevices(new DeviceQuery
{
UserId = userId
- });
+ }).ConfigureAwait(false);
foreach (var info in existing.Items)
{
if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase))
{
- Logout(info);
+ await Logout(info).ConfigureAwait(false);
}
}
}
- public void RevokeToken(string token)
- {
- Logout(token);
- }
-
/// <summary>
/// Reports the capabilities.
/// </summary>
@@ -1572,7 +1643,8 @@ namespace Emby.Server.Implementations.Session
ReportCapabilities(session, capabilities, true);
}
- private void ReportCapabilities(SessionInfo session,
+ private void ReportCapabilities(
+ SessionInfo session,
ClientCapabilities capabilities,
bool saveCapabilities)
{
@@ -1580,36 +1652,19 @@ namespace Emby.Server.Implementations.Session
if (saveCapabilities)
{
- CapabilitiesChanged?.Invoke(this, new SessionEventArgs
- {
- SessionInfo = session
- });
+ CapabilitiesChanged?.Invoke(
+ this,
+ new SessionEventArgs
+ {
+ SessionInfo = session
+ });
- try
- {
- SaveCapabilities(session.DeviceId, capabilities);
- }
- catch (Exception ex)
- {
- _logger.LogError("Error saving device capabilities", ex);
- }
+ _deviceManager.SaveCapabilities(session.DeviceId, capabilities);
}
}
- private ClientCapabilities GetSavedCapabilities(string deviceId)
- {
- return _deviceManager.GetCapabilities(deviceId);
- }
-
- private void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
- {
- _deviceManager.SaveCapabilities(deviceId, capabilities);
- }
-
- private DtoOptions _itemInfoDtoOptions;
-
/// <summary>
- /// Converts a BaseItem to a BaseItemInfo
+ /// Converts a BaseItem to a BaseItemInfo.
/// </summary>
private BaseItemDto GetItemInfo(BaseItem item, MediaSourceInfo mediaSource)
{
@@ -1671,19 +1726,20 @@ namespace Emby.Server.Implementations.Session
return info;
}
- private string GetImageCacheTag(BaseItem item, ImageType type)
+ private string GetImageCacheTag(User user)
{
try
{
- return _imageProcessor.GetImageCacheTag(item, type);
+ return _imageProcessor.GetImageCacheTag(user);
}
- catch (Exception ex)
+ catch (Exception e)
{
- _logger.LogError("Error getting {0} image info", ex, type);
+ _logger.LogError(e, "Error getting image information for profile image");
return null;
}
}
+ /// <inheritdoc />
public void ReportNowViewingItem(string sessionId, string itemId)
{
if (string.IsNullOrEmpty(itemId))
@@ -1691,23 +1747,17 @@ namespace Emby.Server.Implementations.Session
throw new ArgumentNullException(nameof(itemId));
}
- //var item = _libraryManager.GetItemById(new Guid(itemId));
-
- //var info = GetItemInfo(item, null, null);
-
- //ReportNowViewingItem(sessionId, info);
- }
-
- public void ReportNowViewingItem(string sessionId, BaseItemDto item)
- {
- //var session = GetSession(sessionId);
+ var item = _libraryManager.GetItemById(new Guid(itemId));
+ var session = GetSession(sessionId);
- //session.NowViewingItem = item;
+ session.NowViewingItem = GetItemInfo(item, null);
}
+ /// <inheritdoc />
public void ReportTranscodingInfo(string deviceId, TranscodingInfo info)
{
- var session = Sessions.FirstOrDefault(i => string.Equals(i.DeviceId, deviceId));
+ var session = Sessions.FirstOrDefault(i =>
+ string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
if (session != null)
{
@@ -1715,25 +1765,29 @@ namespace Emby.Server.Implementations.Session
}
}
+ /// <inheritdoc />
public void ClearTranscodingInfo(string deviceId)
{
ReportTranscodingInfo(deviceId, null);
}
+ /// <inheritdoc />
public SessionInfo GetSession(string deviceId, string client, string version)
{
- return Sessions.FirstOrDefault(i => string.Equals(i.DeviceId, deviceId) &&
- string.Equals(i.Client, client));
+ return Sessions.FirstOrDefault(i =>
+ string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)
+ && string.Equals(i.Client, client, StringComparison.OrdinalIgnoreCase));
}
- public SessionInfo GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion)
+ /// <inheritdoc />
+ public Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, string appVersion)
{
if (info == null)
{
throw new ArgumentNullException(nameof(info));
}
- var user = info.UserId.Equals(Guid.Empty)
+ var user = info.UserId == Guid.Empty
? null
: _userManager.GetUserById(info.UserId);
@@ -1758,33 +1812,38 @@ namespace Emby.Server.Implementations.Session
return LogSessionActivity(appName, appVersion, deviceId, deviceName, remoteEndpoint, user);
}
- public SessionInfo GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
+ /// <inheritdoc />
+ public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
{
- var result = _authRepo.Get(new AuthenticationInfoQuery
+ var items = (await _deviceManager.GetDevices(new DeviceQuery
{
- AccessToken = token
- });
-
- var info = result.Items.FirstOrDefault();
+ AccessToken = token,
+ Limit = 1
+ }).ConfigureAwait(false)).Items;
- if (info == null)
+ if (items.Count == 0)
{
return null;
}
- return GetSessionByAuthenticationToken(info, deviceId, remoteEndpoint, null);
+ return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false);
}
- public Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
- var adminUserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToList();
+ var adminUserIds = _userManager.Users
+ .Where(i => i.HasPermission(PermissionKind.IsAdministrator))
+ .Select(i => i.Id)
+ .ToList();
return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
}
- public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, CancellationToken cancellationToken)
{
CheckDisposed();
@@ -1795,97 +1854,26 @@ namespace Emby.Server.Implementations.Session
return Task.CompletedTask;
}
- var data = dataFn();
-
- var tasks = sessions.Select(session => Task.Run(async () =>
- {
- try
- {
- await SendMessageToSession(session, name, data, cancellationToken).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError("Error sending message", ex);
- }
-
- }, cancellationToken)).ToArray();
-
- return Task.WhenAll(tasks);
+ return SendMessageToSessions(sessions, name, dataFn(), cancellationToken);
}
- public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
- var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)).ToList();
-
- var tasks = sessions.Select(session => Task.Run(async () =>
- {
- try
- {
- await SendMessageToSession(session, name, data, cancellationToken).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError("Error sending message", ex);
- }
-
- }, cancellationToken)).ToArray();
-
- return Task.WhenAll(tasks);
+ var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser));
+ return SendMessageToSessions(sessions, name, data, cancellationToken);
}
- public Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
- var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)).ToList();
-
- var tasks = sessions.Select(session => Task.Run(async () =>
- {
- try
- {
- await SendMessageToSession(session, name, data, cancellationToken).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError("Error sending message", ex);
- }
-
- }, cancellationToken)).ToArray();
-
- return Task.WhenAll(tasks);
- }
-
- public Task SendMessageToUserDeviceAndAdminSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken)
- {
- CheckDisposed();
-
- var sessions = Sessions
- .Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase) || IsAdminSession(i))
- .ToList();
-
- var tasks = sessions.Select(session => Task.Run(async () =>
- {
- try
- {
- await SendMessageToSession(session, name, data, cancellationToken).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError("Error sending message", ex);
- }
-
- }, cancellationToken)).ToArray();
-
- return Task.WhenAll(tasks);
- }
-
- private bool IsAdminSession(SessionInfo s)
- {
- var user = _userManager.GetUserById(s.UserId);
+ var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
- return user != null && user.Policy.IsAdministrator;
+ return SendMessageToSessions(sessions, name, data, cancellationToken);
}
}
}
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index 24903f5e8..2a14a8c7b 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -1,72 +1,121 @@
+#nullable disable
+
using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Services;
+using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Session;
+using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Session
{
/// <summary>
- /// Class SessionWebSocketListener
+ /// Class SessionWebSocketListener.
/// </summary>
- public class SessionWebSocketListener : IWebSocketListener, IDisposable
+ public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable
{
/// <summary>
- /// The _session manager
+ /// The timeout in seconds after which a WebSocket is considered to be lost.
/// </summary>
- private readonly ISessionManager _sessionManager;
+ private const int WebSocketLostTimeout = 60;
+
+ /// <summary>
+ /// The keep-alive interval factor; controls how often the watcher will check on the status of the WebSockets.
+ /// </summary>
+ private const float IntervalFactor = 0.2f;
+
+ /// <summary>
+ /// The ForceKeepAlive factor; controls when a ForceKeepAlive is sent.
+ /// </summary>
+ private const float ForceKeepAliveFactor = 0.75f;
+
+ /// <summary>
+ /// Lock used for accesing the KeepAlive cancellation token.
+ /// </summary>
+ private readonly object _keepAliveLock = new object();
+
+ /// <summary>
+ /// The WebSocket watchlist.
+ /// </summary>
+ private readonly HashSet<IWebSocketConnection> _webSockets = new HashSet<IWebSocketConnection>();
/// <summary>
- /// The _logger
+ /// Lock used for accessing the WebSockets watchlist.
/// </summary>
- private readonly ILogger _logger;
+ private readonly object _webSocketsLock = new object();
/// <summary>
- /// The _dto service
+ /// The _session manager.
/// </summary>
- private readonly IJsonSerializer _json;
+ private readonly ISessionManager _sessionManager;
- private readonly IHttpServer _httpServer;
+ /// <summary>
+ /// The _logger.
+ /// </summary>
+ private readonly ILogger<SessionWebSocketListener> _logger;
+ private readonly ILoggerFactory _loggerFactory;
+ /// <summary>
+ /// The KeepAlive cancellation token.
+ /// </summary>
+ private CancellationTokenSource _keepAliveCancellationToken;
/// <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)
{
+ _logger = logger;
_sessionManager = sessionManager;
- _logger = loggerFactory.CreateLogger(GetType().Name);
- _json = json;
- _httpServer = httpServer;
- httpServer.WebSocketConnected += _serverManager_WebSocketConnected;
+ _loggerFactory = loggerFactory;
}
- void _serverManager_WebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
+ /// <inheritdoc />
+ public void Dispose()
{
- var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint);
+ StopKeepAlive();
+ }
+ /// <summary>
+ /// Processes the message.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <returns>Task.</returns>
+ public Task ProcessMessageAsync(WebSocketMessageInfo message)
+ => Task.CompletedTask;
+
+ /// <inheritdoc />
+ public async Task ProcessWebSocketConnectedAsync(IWebSocketConnection connection)
+ {
+ var session = await GetSession(connection.QueryString, connection.RemoteEndPoint.ToString()).ConfigureAwait(false);
if (session != null)
{
- EnsureController(session, e.Argument);
+ EnsureController(session, connection);
+ await KeepAliveWebSocket(connection).ConfigureAwait(false);
}
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}", connection.QueryString);
}
}
- private SessionInfo GetSession(QueryParamCollection queryString, string remoteEndpoint)
+ private Task<SessionInfo> GetSession(IQueryCollection queryString, string remoteEndpoint)
{
if (queryString == null)
{
- throw new ArgumentNullException(nameof(queryString));
+ return null;
}
var token = queryString["api_key"];
@@ -74,31 +123,224 @@ namespace Emby.Server.Implementations.Session
{
return null;
}
+
var deviceId = queryString["deviceId"];
return _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint);
}
- public void Dispose()
+ private void EnsureController(SessionInfo session, IWebSocketConnection connection)
{
- _httpServer.WebSocketConnected -= _serverManager_WebSocketConnected;
+ var controllerInfo = session.EnsureController<WebSocketController>(
+ s => new WebSocketController(_loggerFactory.CreateLogger<WebSocketController>(), s, _sessionManager));
+
+ var controller = (WebSocketController)controllerInfo.Item1;
+ controller.AddWebSocket(connection);
+
+ _sessionManager.OnSessionControllerConnected(session);
}
/// <summary>
- /// Processes the message.
+ /// Called when a WebSocket is closed.
/// </summary>
- /// <param name="message">The message.</param>
+ /// <param name="sender">The WebSocket.</param>
+ /// <param name="e">The event arguments.</param>
+ private void OnWebSocketClosed(object sender, EventArgs e)
+ {
+ var webSocket = (IWebSocketConnection)sender;
+ _logger.LogDebug("WebSocket {0} is closed.", webSocket);
+ RemoveWebSocket(webSocket);
+ }
+
+ /// <summary>
+ /// Adds a WebSocket to the KeepAlive watchlist.
+ /// </summary>
+ /// <param name="webSocket">The WebSocket to monitor.</param>
+ private async Task KeepAliveWebSocket(IWebSocketConnection webSocket)
+ {
+ lock (_webSocketsLock)
+ {
+ if (!_webSockets.Add(webSocket))
+ {
+ _logger.LogWarning("Multiple attempts to keep alive single WebSocket {0}", webSocket);
+ return;
+ }
+
+ webSocket.Closed += OnWebSocketClosed;
+ webSocket.LastKeepAliveDate = DateTime.UtcNow;
+
+ StartKeepAlive();
+ }
+
+ // Notify WebSocket about timeout
+ try
+ {
+ await SendForceKeepAlive(webSocket).ConfigureAwait(false);
+ }
+ catch (WebSocketException exception)
+ {
+ _logger.LogWarning(exception, "Cannot send ForceKeepAlive message to WebSocket {0}.", webSocket);
+ }
+ }
+
+ /// <summary>
+ /// Removes a WebSocket from the KeepAlive watchlist.
+ /// </summary>
+ /// <param name="webSocket">The WebSocket to remove.</param>
+ private void RemoveWebSocket(IWebSocketConnection webSocket)
+ {
+ lock (_webSocketsLock)
+ {
+ if (!_webSockets.Remove(webSocket))
+ {
+ _logger.LogWarning("WebSocket {0} not on watchlist.", webSocket);
+ }
+ else
+ {
+ webSocket.Closed -= OnWebSocketClosed;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Starts the KeepAlive watcher.
+ /// </summary>
+ private void StartKeepAlive()
+ {
+ lock (_keepAliveLock)
+ {
+ if (_keepAliveCancellationToken == null)
+ {
+ _keepAliveCancellationToken = new CancellationTokenSource();
+ // Start KeepAlive watcher
+ _ = RepeatAsyncCallbackEvery(
+ KeepAliveSockets,
+ TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor),
+ _keepAliveCancellationToken.Token);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Stops the KeepAlive watcher.
+ /// </summary>
+ private void StopKeepAlive()
+ {
+ lock (_keepAliveLock)
+ {
+ if (_keepAliveCancellationToken != null)
+ {
+ _keepAliveCancellationToken.Cancel();
+ _keepAliveCancellationToken.Dispose();
+ _keepAliveCancellationToken = null;
+ }
+ }
+
+ lock (_webSocketsLock)
+ {
+ foreach (var webSocket in _webSockets)
+ {
+ webSocket.Closed -= OnWebSocketClosed;
+ }
+
+ _webSockets.Clear();
+ }
+ }
+
+ /// <summary>
+ /// Checks status of KeepAlive of WebSockets.
+ /// </summary>
+ private async Task KeepAliveSockets()
+ {
+ List<IWebSocketConnection> inactive;
+ List<IWebSocketConnection> lost;
+
+ lock (_webSocketsLock)
+ {
+ _logger.LogDebug("Watching {0} WebSockets.", _webSockets.Count);
+
+ inactive = _webSockets.Where(i =>
+ {
+ var elapsed = (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds;
+ return (elapsed > WebSocketLostTimeout * ForceKeepAliveFactor) && (elapsed < WebSocketLostTimeout);
+ }).ToList();
+ lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout).ToList();
+ }
+
+ if (inactive.Count > 0)
+ {
+ _logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
+ }
+
+ foreach (var webSocket in inactive)
+ {
+ try
+ {
+ await SendForceKeepAlive(webSocket).ConfigureAwait(false);
+ }
+ catch (WebSocketException exception)
+ {
+ _logger.LogInformation(exception, "Error sending ForceKeepAlive message to WebSocket.");
+ lost.Add(webSocket);
+ }
+ }
+
+ lock (_webSocketsLock)
+ {
+ if (lost.Count > 0)
+ {
+ _logger.LogInformation("Lost {0} WebSockets.", lost.Count);
+ foreach (var webSocket in lost)
+ {
+ // TODO: handle session relative to the lost webSocket
+ RemoveWebSocket(webSocket);
+ }
+ }
+
+ if (_webSockets.Count == 0)
+ {
+ StopKeepAlive();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Sends a ForceKeepAlive message to a WebSocket.
+ /// </summary>
+ /// <param name="webSocket">The WebSocket.</param>
/// <returns>Task.</returns>
- public Task ProcessMessage(WebSocketMessageInfo message)
+ private Task SendForceKeepAlive(IWebSocketConnection webSocket)
{
- return Task.CompletedTask;
+ return webSocket.SendAsync(
+ new WebSocketMessage<int>
+ {
+ MessageType = SessionMessageType.ForceKeepAlive,
+ Data = WebSocketLostTimeout
+ },
+ CancellationToken.None);
}
- private void EnsureController(SessionInfo session, IWebSocketConnection connection)
+ /// <summary>
+ /// Runs a given async callback once every specified interval time, until cancelled.
+ /// </summary>
+ /// <param name="callback">The async callback.</param>
+ /// <param name="interval">The interval time.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task RepeatAsyncCallbackEvery(Func<Task> callback, TimeSpan interval, CancellationToken cancellationToken)
{
- var controllerInfo = session.EnsureController<WebSocketController>(s => new WebSocketController(s, _logger, _sessionManager));
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ await callback().ConfigureAwait(false);
- var controller = (WebSocketController)controllerInfo.Item1;
- controller.AddWebSocket(connection);
+ try
+ {
+ await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
+ }
+ catch (TaskCanceledException)
+ {
+ return;
+ }
+ }
}
}
}
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index 0d483c55f..9fa92a53a 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
using System;
using System.Collections.Generic;
using System.Linq;
@@ -7,64 +9,68 @@ using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Session;
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 ILogger<WebSocketController> _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);
+ var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
+ _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>(
+ SessionMessageType name,
+ Guid messageId,
+ T data,
+ CancellationToken cancellationToken)
{
var socket = GetActiveSockets()
+ .OrderByDescending(i => i.LastActivityDate)
.FirstOrDefault();
if (socket == null)
@@ -72,21 +78,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;
}
}
}