aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Controller')
-rw-r--r--MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs8
-rw-r--r--MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs65
-rw-r--r--MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs5
-rw-r--r--MediaBrowser.Controller/Collections/ICollectionManager.cs7
-rw-r--r--MediaBrowser.Controller/Drawing/IImageProcessor.cs2
-rw-r--r--MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs6
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs3
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs42
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs42
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs42
-rw-r--r--MediaBrowser.Controller/Entities/ItemImageInfo.cs8
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs2
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs4
-rw-r--r--MediaBrowser.Controller/Entities/Video.cs2
-rw-r--r--MediaBrowser.Controller/Events/Authentication/AuthenticationRequestEventArgs.cs60
-rw-r--r--MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs38
-rw-r--r--MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs13
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs5
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs16
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs2
-rw-r--r--MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs17
-rw-r--r--MediaBrowser.Controller/Lyrics/ILyricParser.cs28
-rw-r--r--MediaBrowser.Controller/Lyrics/ILyricProvider.cs36
-rw-r--r--MediaBrowser.Controller/Lyrics/LyricFile.cs28
-rw-r--r--MediaBrowser.Controller/Lyrics/LyricInfo.cs49
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs686
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs10
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs4
-rw-r--r--MediaBrowser.Controller/MediaEncoding/JobLogger.cs4
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs7
-rw-r--r--MediaBrowser.Controller/Net/IWebSocketConnection.cs30
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessage.cs22
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessageInfo.cs4
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessageOfT.cs32
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/IInboundWebSocketMessage.cs10
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/IOutboundWebSocketMessage.cs10
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs25
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs14
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Inbound/InboundKeepAliveMessage.cs14
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs14
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs14
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs8
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessageOfT.cs26
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs25
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs23
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs23
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/OutboundKeepAliveMessage.cs14
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs23
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs23
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs14
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs25
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs14
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs14
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs25
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs25
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs25
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs25
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs25
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs23
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs24
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs14
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessageOfT.cs33
-rw-r--r--MediaBrowser.Controller/Playlists/IPlaylistManager.cs13
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs8
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs17
-rw-r--r--MediaBrowser.Controller/Resolvers/IItemResolver.cs2
-rw-r--r--MediaBrowser.Controller/Resolvers/ItemResolver.cs6
-rw-r--r--MediaBrowser.Controller/Security/IAuthenticationManager.cs4
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs14
-rw-r--r--MediaBrowser.Controller/Subtitles/SubtitleResponse.cs2
-rw-r--r--MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs24
89 files changed, 1795 insertions, 586 deletions
diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
index a56d3c822..81b532fda 100644
--- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
+++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System.Threading.Tasks;
@@ -23,7 +21,7 @@ namespace MediaBrowser.Controller.Authentication
public interface IRequiresResolvedUser
{
- Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser);
+ Task<ProviderAuthenticationResult> Authenticate(string username, string password, User? resolvedUser);
}
public interface IHasNewUserPolicy
@@ -33,8 +31,8 @@ namespace MediaBrowser.Controller.Authentication
public class ProviderAuthenticationResult
{
- public string Username { get; set; }
+ public required string Username { get; set; }
- public string DisplayName { get; set; }
+ public string? DisplayName { get; set; }
}
}
diff --git a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
index ed7c2c2c1..b263c173e 100644
--- a/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
+++ b/MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
@@ -1,11 +1,10 @@
using System;
-using System.Diagnostics.CodeAnalysis;
using System.Linq;
-using System.Threading;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
namespace MediaBrowser.Controller.BaseItemManager
@@ -15,8 +14,6 @@ namespace MediaBrowser.Controller.BaseItemManager
{
private readonly IServerConfigurationManager _serverConfigurationManager;
- private int _metadataRefreshConcurrency;
-
/// <summary>
/// Initializes a new instance of the <see cref="BaseItemManager"/> class.
/// </summary>
@@ -24,17 +21,9 @@ namespace MediaBrowser.Controller.BaseItemManager
public BaseItemManager(IServerConfigurationManager serverConfigurationManager)
{
_serverConfigurationManager = serverConfigurationManager;
-
- _metadataRefreshConcurrency = GetMetadataRefreshConcurrency();
- SetupMetadataThrottler();
-
- _serverConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
}
/// <inheritdoc />
- public SemaphoreSlim MetadataRefreshThrottler { get; private set; }
-
- /// <inheritdoc />
public bool IsMetadataFetcherEnabled(BaseItem baseItem, TypeOptions? libraryTypeOptions, string name)
{
if (baseItem is Channel)
@@ -51,12 +40,11 @@ namespace MediaBrowser.Controller.BaseItemManager
if (libraryTypeOptions is not null)
{
- return libraryTypeOptions.MetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
+ return libraryTypeOptions.MetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase);
}
- var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase));
-
- return itemConfig is null || !itemConfig.DisabledMetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
+ var itemConfig = _serverConfigurationManager.GetMetadataOptionsForType(baseItem.GetType().Name);
+ return itemConfig is null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase);
}
/// <inheritdoc />
@@ -76,50 +64,11 @@ namespace MediaBrowser.Controller.BaseItemManager
if (libraryTypeOptions is not null)
{
- return libraryTypeOptions.ImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
- }
-
- var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase));
-
- return itemConfig is null || !itemConfig.DisabledImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
- }
-
- /// <summary>
- /// Called when the configuration is updated.
- /// It will refresh the metadata throttler if the relevant config changed.
- /// </summary>
- private void OnConfigurationUpdated(object? sender, EventArgs e)
- {
- int newMetadataRefreshConcurrency = GetMetadataRefreshConcurrency();
- if (_metadataRefreshConcurrency != newMetadataRefreshConcurrency)
- {
- _metadataRefreshConcurrency = newMetadataRefreshConcurrency;
- SetupMetadataThrottler();
- }
- }
-
- /// <summary>
- /// Creates the metadata refresh throttler.
- /// </summary>
- [MemberNotNull(nameof(MetadataRefreshThrottler))]
- private void SetupMetadataThrottler()
- {
- MetadataRefreshThrottler = new SemaphoreSlim(_metadataRefreshConcurrency);
- }
-
- /// <summary>
- /// Returns the metadata refresh concurrency.
- /// </summary>
- private int GetMetadataRefreshConcurrency()
- {
- var concurrency = _serverConfigurationManager.Configuration.LibraryMetadataRefreshConcurrency;
-
- if (concurrency <= 0)
- {
- concurrency = Environment.ProcessorCount;
+ return libraryTypeOptions.ImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase);
}
- return concurrency;
+ var itemConfig = _serverConfigurationManager.GetMetadataOptionsForType(baseItem.GetType().Name);
+ return itemConfig is null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase);
}
}
}
diff --git a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
index b07c80879..ac20120d9 100644
--- a/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
+++ b/MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs
@@ -10,11 +10,6 @@ namespace MediaBrowser.Controller.BaseItemManager
public interface IBaseItemManager
{
/// <summary>
- /// Gets the semaphore used to limit the amount of concurrent metadata refreshes.
- /// </summary>
- SemaphoreSlim MetadataRefreshThrottler { get; }
-
- /// <summary>
/// Is metadata fetcher enabled.
/// </summary>
/// <param name="baseItem">The base item.</param>
diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs
index b8c33ee5a..38a78a67b 100644
--- a/MediaBrowser.Controller/Collections/ICollectionManager.cs
+++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs
@@ -56,5 +56,12 @@ namespace MediaBrowser.Controller.Collections
/// <param name="user">The user.</param>
/// <returns>IEnumerable{BaseItem}.</returns>
IEnumerable<BaseItem> CollapseItemsWithinBoxSets(IEnumerable<BaseItem> items, User user);
+
+ /// <summary>
+ /// Gets the folder where collections are stored.
+ /// </summary>
+ /// <param name="createIfNeeded">Will create the collection folder on the storage if set to true.</param>
+ /// <returns>The folder instance referencing the collection storage.</returns>
+ Task<Folder?> GetCollectionsFolder(bool createIfNeeded);
}
}
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index e5ce0aa21..cdc3d52b9 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -66,7 +66,7 @@ namespace MediaBrowser.Controller.Drawing
/// <returns>Guid.</returns>
string GetImageCacheTag(BaseItem item, ItemImageInfo image);
- string GetImageCacheTag(BaseItem item, ChapterInfo chapter);
+ string? GetImageCacheTag(BaseItem item, ChapterInfo chapter);
string? GetImageCacheTag(User user);
diff --git a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs
index 62b70ce53..10326363a 100644
--- a/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs
+++ b/MediaBrowser.Controller/Drawing/ImageProcessorExtensions.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using MediaBrowser.Controller.Entities;
@@ -9,12 +7,12 @@ namespace MediaBrowser.Controller.Drawing
{
public static class ImageProcessorExtensions
{
- public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType)
+ public static string? GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType)
{
return processor.GetImageCacheTag(item, imageType, 0);
}
- public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType, int imageIndex)
+ public static string? GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType, int imageIndex)
{
var imageInfo = item.GetImageInfo(imageType, imageIndex);
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
index 2dbd513a1..237345206 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
@@ -183,6 +183,9 @@ namespace MediaBrowser.Controller.Entities.Audio
progress.Report(percent * 95);
}
+ // get album LUFS
+ LUFS = items.OfType<Audio>().Max(item => item.LUFS);
+
var parentRefreshOptions = refreshOptions;
if (childUpdateType > ItemUpdateType.None)
{
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
index 15a79fa1f..18d948a62 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
@@ -59,7 +59,7 @@ namespace MediaBrowser.Controller.Entities.Audio
{
if (IsAccessedByName)
{
- return new List<BaseItem>();
+ return Enumerable.Empty<BaseItem>();
}
return base.Children;
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index adc7b2f95..9f3e8eec9 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -129,6 +129,13 @@ namespace MediaBrowser.Controller.Entities
public string Album { get; set; }
/// <summary>
+ /// Gets or sets the LUFS value.
+ /// </summary>
+ /// <value>The LUFS Value.</value>
+ [JsonIgnore]
+ public float LUFS { get; set; }
+
+ /// <summary>
/// Gets or sets the channel identifier.
/// </summary>
/// <value>The channel identifier.</value>
@@ -1237,14 +1244,6 @@ namespace MediaBrowser.Controller.Entities
return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
}
- protected virtual void TriggerOnRefreshStart()
- {
- }
-
- protected virtual void TriggerOnRefreshComplete()
- {
- }
-
/// <summary>
/// Overrides the base implementation to refresh metadata for local trailers.
/// </summary>
@@ -1253,8 +1252,6 @@ namespace MediaBrowser.Controller.Entities
/// <returns>true if a provider reports we changed.</returns>
public async Task<ItemUpdateType> RefreshMetadata(MetadataRefreshOptions options, CancellationToken cancellationToken)
{
- TriggerOnRefreshStart();
-
var requiresSave = false;
if (SupportsOwnedItems)
@@ -1274,21 +1271,14 @@ namespace MediaBrowser.Controller.Entities
}
}
- try
- {
- var refreshOptions = requiresSave
- ? new MetadataRefreshOptions(options)
- {
- ForceSave = true
- }
- : options;
+ var refreshOptions = requiresSave
+ ? new MetadataRefreshOptions(options)
+ {
+ ForceSave = true
+ }
+ : options;
- return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
- }
- finally
- {
- TriggerOnRefreshComplete();
- }
+ return await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
}
protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
@@ -1360,7 +1350,7 @@ namespace MediaBrowser.Controller.Entities
private async Task<bool> RefreshExtras(BaseItem item, MetadataRefreshOptions options, IReadOnlyList<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
{
var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray();
- var newExtraIds = extras.Select(i => i.Id).ToArray();
+ var newExtraIds = Array.ConvertAll(extras, x => x.Id);
var extrasChanged = !item.ExtraIds.SequenceEqual(newExtraIds);
if (!extrasChanged && !options.ReplaceAllMetadata && options.MetadataRefreshMode != MetadataRefreshMode.FullRefresh)
@@ -1874,7 +1864,7 @@ namespace MediaBrowser.Controller.Entities
/// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception>
public bool HasImage(ImageType type, int imageIndex)
{
- return GetImageInfo(type, imageIndex) != null;
+ return GetImageInfo(type, imageIndex) is not null;
}
public void SetImage(ItemImageInfo image, int index)
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index 095b261c0..f51162f9d 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -29,7 +30,7 @@ namespace MediaBrowser.Controller.Entities
public class CollectionFolder : Folder, ICollectionFolder
{
private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
- private static readonly Dictionary<string, LibraryOptions> _libraryOptions = new Dictionary<string, LibraryOptions>();
+ private static readonly ConcurrentDictionary<string, LibraryOptions> _libraryOptions = new ConcurrentDictionary<string, LibraryOptions>();
private bool _requiresRefresh;
/// <summary>
@@ -139,45 +140,26 @@ namespace MediaBrowser.Controller.Entities
}
public static LibraryOptions GetLibraryOptions(string path)
- {
- lock (_libraryOptions)
- {
- if (!_libraryOptions.TryGetValue(path, out var options))
- {
- options = LoadLibraryOptions(path);
- _libraryOptions[path] = options;
- }
-
- return options;
- }
- }
+ => _libraryOptions.GetOrAdd(path, LoadLibraryOptions);
public static void SaveLibraryOptions(string path, LibraryOptions options)
{
- lock (_libraryOptions)
- {
- _libraryOptions[path] = options;
+ _libraryOptions[path] = options;
- var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
- foreach (var mediaPath in clone.PathInfos)
+ var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
+ foreach (var mediaPath in clone.PathInfos)
+ {
+ if (!string.IsNullOrEmpty(mediaPath.Path))
{
- if (!string.IsNullOrEmpty(mediaPath.Path))
- {
- mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path);
- }
+ mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path);
}
-
- XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path));
}
+
+ XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path));
}
public static void OnCollectionFolderChange()
- {
- lock (_libraryOptions)
- {
- _libraryOptions.Clear();
- }
- }
+ => _libraryOptions.Clear();
public override bool IsSaveLocalMetadataEnabled()
{
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index bccb4107f..44fe65103 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -301,14 +301,6 @@ namespace MediaBrowser.Controller.Entities
return dictionary;
}
- protected override void TriggerOnRefreshStart()
- {
- }
-
- protected override void TriggerOnRefreshComplete()
- {
- }
-
/// <summary>
/// Validates the children internal.
/// </summary>
@@ -510,26 +502,17 @@ namespace MediaBrowser.Controller.Entities
private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
{
- // limit the amount of concurrent metadata refreshes
- await ProviderManager.RunMetadataRefresh(
- async () =>
- {
- var series = container as Series;
- if (series is not null)
- {
- await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
- }
+ if (container is Series series)
+ {
+ await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
+ }
- await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false);
- },
- cancellationToken).ConfigureAwait(false);
+ await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false);
}
private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken)
{
- var container = child as IMetadataContainer;
-
- if (container is not null)
+ if (child is IMetadataContainer container)
{
await RefreshAllMetadataForContainer(container, refreshOptions, progress, cancellationToken).ConfigureAwait(false);
}
@@ -537,10 +520,7 @@ namespace MediaBrowser.Controller.Entities
{
if (refreshOptions.RefreshItem(child))
{
- // limit the amount of concurrent metadata refreshes
- await ProviderManager.RunMetadataRefresh(
- async () => await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false),
- cancellationToken).ConfigureAwait(false);
+ await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
}
if (recursive && child is Folder folder)
@@ -586,7 +566,7 @@ namespace MediaBrowser.Controller.Entities
}
var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
- var parallelism = fanoutConcurrency == 0 ? Environment.ProcessorCount : fanoutConcurrency;
+ var parallelism = fanoutConcurrency > 0 ? fanoutConcurrency : 2 * Environment.ProcessorCount;
var actionBlock = new ActionBlock<int>(
async i =>
@@ -618,7 +598,7 @@ namespace MediaBrowser.Controller.Entities
for (var i = 0; i < childrenCount; i++)
{
- actionBlock.Post(i);
+ await actionBlock.SendAsync(i).ConfigureAwait(false);
}
actionBlock.Complete();
@@ -730,7 +710,7 @@ namespace MediaBrowser.Controller.Entities
return LibraryManager.GetItemsResult(query);
}
- private QueryResult<BaseItem> QueryWithPostFiltering2(InternalItemsQuery query)
+ protected QueryResult<BaseItem> QueryWithPostFiltering2(InternalItemsQuery query)
{
var startIndex = query.StartIndex;
var limit = query.Limit;
@@ -1272,7 +1252,7 @@ namespace MediaBrowser.Controller.Entities
{
ArgumentNullException.ThrowIfNull(user);
- return GetChildren(user, includeLinkedChildren, null);
+ return GetChildren(user, includeLinkedChildren, new InternalItemsQuery(user));
}
public virtual List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs
index 0171af27c..1d45d4da0 100644
--- a/MediaBrowser.Controller/Entities/ItemImageInfo.cs
+++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -14,7 +12,7 @@ namespace MediaBrowser.Controller.Entities
/// Gets or sets the path.
/// </summary>
/// <value>The path.</value>
- public string Path { get; set; }
+ public required string Path { get; set; }
/// <summary>
/// Gets or sets the type.
@@ -36,9 +34,9 @@ namespace MediaBrowser.Controller.Entities
/// Gets or sets the blurhash.
/// </summary>
/// <value>The blurhash.</value>
- public string BlurHash { get; set; }
+ public string? BlurHash { get; set; }
[JsonIgnore]
- public bool IsLocalFile => Path is null || !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase);
+ public bool IsLocalFile => !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase);
}
}
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index 597b4cecb..bf31508c1 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -99,7 +99,7 @@ namespace MediaBrowser.Controller.Entities.TV
}
[JsonIgnore]
- public bool IsInSeasonFolder => FindParent<Season>() != null;
+ public bool IsInSeasonFolder => FindParent<Season>() is not null;
[JsonIgnore]
public string SeriesPresentationUniqueKey { get; set; }
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index e7a8a773e..a49c1609d 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -28,6 +28,7 @@ namespace MediaBrowser.Controller.Entities.TV
public Series()
{
AirDays = Array.Empty<DayOfWeek>();
+ SeasonNames = new Dictionary<int, string>();
}
public DayOfWeek[] AirDays { get; set; }
@@ -35,6 +36,9 @@ namespace MediaBrowser.Controller.Entities.TV
public string AirTime { get; set; }
[JsonIgnore]
+ public Dictionary<int, string> SeasonNames { get; set; }
+
+ [JsonIgnore]
public override bool SupportsAddingToPlaylist => true;
[JsonIgnore]
diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs
index 5b7abea10..9f685b7e2 100644
--- a/MediaBrowser.Controller/Entities/Video.cs
+++ b/MediaBrowser.Controller/Entities/Video.cs
@@ -333,7 +333,7 @@ namespace MediaBrowser.Controller.Entities
protected override bool IsActiveRecording()
{
- return LiveTvManager.GetActiveRecordingInfo(Path) != null;
+ return LiveTvManager.GetActiveRecordingInfo(Path) is not null;
}
public override bool CanDelete()
diff --git a/MediaBrowser.Controller/Events/Authentication/AuthenticationRequestEventArgs.cs b/MediaBrowser.Controller/Events/Authentication/AuthenticationRequestEventArgs.cs
new file mode 100644
index 000000000..2143c6998
--- /dev/null
+++ b/MediaBrowser.Controller/Events/Authentication/AuthenticationRequestEventArgs.cs
@@ -0,0 +1,60 @@
+using System;
+using MediaBrowser.Controller.Session;
+
+namespace MediaBrowser.Controller.Events.Authentication;
+
+/// <summary>
+/// A class representing an authentication result event.
+/// </summary>
+public class AuthenticationRequestEventArgs : EventArgs
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AuthenticationRequestEventArgs"/> class.
+ /// </summary>
+ /// <param name="request">The <see cref="AuthenticationRequest"/>.</param>
+ public AuthenticationRequestEventArgs(AuthenticationRequest request)
+ {
+ Username = request.Username;
+ UserId = request.UserId;
+ App = request.App;
+ AppVersion = request.AppVersion;
+ DeviceId = request.DeviceId;
+ DeviceName = request.DeviceName;
+ RemoteEndPoint = request.RemoteEndPoint;
+ }
+
+ /// <summary>
+ /// Gets or sets the user name.
+ /// </summary>
+ public string? Username { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ public Guid? UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the app.
+ /// </summary>
+ public string? App { get; set; }
+
+ /// <summary>
+ /// Gets or sets the app version.
+ /// </summary>
+ public string? AppVersion { get; set; }
+
+ /// <summary>
+ /// Gets or sets the device id.
+ /// </summary>
+ public string? DeviceId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the device name.
+ /// </summary>
+ public string? DeviceName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the remote endpoint.
+ /// </summary>
+ public string? RemoteEndPoint { get; set; }
+}
diff --git a/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs
new file mode 100644
index 000000000..357ef9406
--- /dev/null
+++ b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs
@@ -0,0 +1,38 @@
+using System;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Controller.Events.Authentication;
+
+/// <summary>
+/// A class representing an authentication result event.
+/// </summary>
+public class AuthenticationResultEventArgs : EventArgs
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AuthenticationResultEventArgs"/> class.
+ /// </summary>
+ /// <param name="result">The <see cref="AuthenticationResult"/>.</param>
+ public AuthenticationResultEventArgs(AuthenticationResult result)
+ {
+ User = result.User;
+ SessionInfo = result.SessionInfo;
+ ServerId = result.ServerId;
+ }
+
+ /// <summary>
+ /// Gets or sets the user.
+ /// </summary>
+ public UserDto User { get; set; }
+
+ /// <summary>
+ /// Gets or sets the session information.
+ /// </summary>
+ public SessionInfo? SessionInfo { get; set; }
+
+ /// <summary>
+ /// Gets or sets the server id.
+ /// </summary>
+ public string? ServerId { get; set; }
+}
diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
index 3b5e8ece7..6c58064ce 100644
--- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
@@ -60,6 +60,11 @@ namespace MediaBrowser.Controller.Extensions
public const string UnixSocketPermissionsKey = "kestrel:socketPermissions";
/// <summary>
+ /// The cache size of the SQL database, see cache_size.
+ /// </summary>
+ public const string SqliteCacheSizeKey = "sqlite:cacheSize";
+
+ /// <summary>
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
/// </summary>
/// <param name="configuration">The configuration to retrieve the value from.</param>
@@ -115,5 +120,13 @@ namespace MediaBrowser.Controller.Extensions
/// <returns>The unix socket permissions.</returns>
public static string? GetUnixSocketPermissions(this IConfiguration configuration)
=> configuration[UnixSocketPermissionsKey];
+
+ /// <summary>
+ /// Gets the cache_size from the <see cref="IConfiguration" />.
+ /// </summary>
+ /// <param name="configuration">The configuration to read the setting from.</param>
+ /// <returns>The sqlite cache size.</returns>
+ public static int? GetSqliteCacheSize(this IConfiguration configuration)
+ => configuration.GetValue<int?>(SqliteCacheSizeKey);
}
}
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index 11afdc4ae..45ac5c3a8 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -4,7 +4,6 @@
using System.Net;
using MediaBrowser.Common;
-using MediaBrowser.Common.Net;
using MediaBrowser.Model.System;
using Microsoft.AspNetCore.Http;
@@ -75,10 +74,10 @@ namespace MediaBrowser.Controller
/// <summary>
/// Gets an URL that can be used to access the API over LAN.
/// </summary>
- /// <param name="hostname">An optional hostname to use.</param>
+ /// <param name="ipAddress">An optional IP address to use.</param>
/// <param name="allowHttps">A value indicating whether to allow HTTPS.</param>
/// <returns>The API URL.</returns>
- string GetApiUrlForLocalAccess(IPObject hostname = null, bool allowHttps = true);
+ string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true);
/// <summary>
/// Gets a local (LAN) URL that can be used to access the API.
diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs
index 37b4afcf3..6d6a532db 100644
--- a/MediaBrowser.Controller/Library/IUserManager.cs
+++ b/MediaBrowser.Controller/Library/IUserManager.cs
@@ -97,13 +97,6 @@ namespace MediaBrowser.Controller.Library
Task ResetPassword(User user);
/// <summary>
- /// Resets the easy password.
- /// </summary>
- /// <param name="user">The user.</param>
- /// <returns>Task.</returns>
- Task ResetEasyPassword(User user);
-
- /// <summary>
/// Changes the password.
/// </summary>
/// <param name="user">The user.</param>
@@ -112,15 +105,6 @@ namespace MediaBrowser.Controller.Library
Task ChangePassword(User user, string newPassword);
/// <summary>
- /// Changes the easy password.
- /// </summary>
- /// <param name="user">The user.</param>
- /// <param name="newPassword">New password to use.</param>
- /// <param name="newPasswordSha1">Hash of new password.</param>
- /// <returns>Task.</returns>
- Task ChangeEasyPassword(User user, string newPassword, string newPasswordSha1);
-
- /// <summary>
/// Gets the user dto.
/// </summary>
/// <param name="user">The user.</param>
diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
index c70102167..dcd0110fb 100644
--- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -217,7 +217,7 @@ namespace MediaBrowser.Controller.Library
/// <returns><c>true</c> if [contains file system entry by name] [the specified name]; otherwise, <c>false</c>.</returns>
public bool ContainsFileSystemEntryByName(string name)
{
- return GetFileSystemEntryByName(name) != null;
+ return GetFileSystemEntryByName(name) is not null;
}
public string GetCollectionType()
diff --git a/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs b/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs
index 41cfcae16..ee9420cb4 100644
--- a/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs
+++ b/MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs
@@ -1,8 +1,8 @@
-#nullable disable
-
#pragma warning disable CS1591
+using System;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Configuration;
namespace MediaBrowser.Controller.Library
@@ -10,8 +10,15 @@ namespace MediaBrowser.Controller.Library
public static class MetadataConfigurationExtensions
{
public static MetadataConfiguration GetMetadataConfiguration(this IConfigurationManager config)
- {
- return config.GetConfiguration<MetadataConfiguration>("metadata");
- }
+ => config.GetConfiguration<MetadataConfiguration>("metadata");
+
+ /// <summary>
+ /// Gets the <see cref="MetadataOptions" /> for the specified type.
+ /// </summary>
+ /// <param name="config">The <see cref="IServerConfigurationManager"/>.</param>
+ /// <param name="type">The type to get the <see cref="MetadataOptions" /> for.</param>
+ /// <returns>The <see cref="MetadataOptions" /> for the specified type or <c>null</c>.</returns>
+ public static MetadataOptions? GetMetadataOptionsForType(this IServerConfigurationManager config, string type)
+ => Array.Find(config.Configuration.MetadataOptions, i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase));
}
}
diff --git a/MediaBrowser.Controller/Lyrics/ILyricParser.cs b/MediaBrowser.Controller/Lyrics/ILyricParser.cs
new file mode 100644
index 000000000..65a9471a3
--- /dev/null
+++ b/MediaBrowser.Controller/Lyrics/ILyricParser.cs
@@ -0,0 +1,28 @@
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Providers.Lyric;
+
+namespace MediaBrowser.Controller.Lyrics;
+
+/// <summary>
+/// Interface ILyricParser.
+/// </summary>
+public interface ILyricParser
+{
+ /// <summary>
+ /// Gets a value indicating the provider name.
+ /// </summary>
+ string Name { get; }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ ResolverPriority Priority { get; }
+
+ /// <summary>
+ /// Parses the raw lyrics into a response.
+ /// </summary>
+ /// <param name="lyrics">The raw lyrics content.</param>
+ /// <returns>The parsed lyrics or null if invalid.</returns>
+ LyricResponse? ParseLyrics(LyricFile lyrics);
+}
diff --git a/MediaBrowser.Controller/Lyrics/ILyricProvider.cs b/MediaBrowser.Controller/Lyrics/ILyricProvider.cs
deleted file mode 100644
index 2a04c6152..000000000
--- a/MediaBrowser.Controller/Lyrics/ILyricProvider.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Resolvers;
-
-namespace MediaBrowser.Controller.Lyrics;
-
-/// <summary>
-/// Interface ILyricsProvider.
-/// </summary>
-public interface ILyricProvider
-{
- /// <summary>
- /// Gets a value indicating the provider name.
- /// </summary>
- string Name { get; }
-
- /// <summary>
- /// Gets the priority.
- /// </summary>
- /// <value>The priority.</value>
- ResolverPriority Priority { get; }
-
- /// <summary>
- /// Gets the supported media types for this provider.
- /// </summary>
- /// <value>The supported media types.</value>
- IReadOnlyCollection<string> SupportedMediaTypes { get; }
-
- /// <summary>
- /// Gets the lyrics.
- /// </summary>
- /// <param name="item">The media item.</param>
- /// <returns>A task representing found lyrics.</returns>
- Task<LyricResponse?> GetLyrics(BaseItem item);
-}
diff --git a/MediaBrowser.Controller/Lyrics/LyricFile.cs b/MediaBrowser.Controller/Lyrics/LyricFile.cs
new file mode 100644
index 000000000..ede89403c
--- /dev/null
+++ b/MediaBrowser.Controller/Lyrics/LyricFile.cs
@@ -0,0 +1,28 @@
+namespace MediaBrowser.Providers.Lyric;
+
+/// <summary>
+/// The information for a raw lyrics file before parsing.
+/// </summary>
+public class LyricFile
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LyricFile"/> class.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="content">The content, must not be empty.</param>
+ public LyricFile(string name, string content)
+ {
+ Name = name;
+ Content = content;
+ }
+
+ /// <summary>
+ /// Gets or sets the name of the lyrics file. This must include the file extension.
+ /// </summary>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the contents of the file.
+ /// </summary>
+ public string Content { get; set; }
+}
diff --git a/MediaBrowser.Controller/Lyrics/LyricInfo.cs b/MediaBrowser.Controller/Lyrics/LyricInfo.cs
deleted file mode 100644
index 6ec6df582..000000000
--- a/MediaBrowser.Controller/Lyrics/LyricInfo.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using System;
-using System.IO;
-using Jellyfin.Extensions;
-
-namespace MediaBrowser.Controller.Lyrics;
-
-/// <summary>
-/// Lyric helper methods.
-/// </summary>
-public static class LyricInfo
-{
- /// <summary>
- /// Gets matching lyric file for a requested item.
- /// </summary>
- /// <param name="lyricProvider">The lyricProvider interface to use.</param>
- /// <param name="itemPath">Path of requested item.</param>
- /// <returns>Lyric file path if passed lyric provider's supported media type is found; otherwise, null.</returns>
- public static string? GetLyricFilePath(this ILyricProvider lyricProvider, string itemPath)
- {
- // Ensure we have a provider
- if (lyricProvider is null)
- {
- return null;
- }
-
- // Ensure the path to the item is not null
- string? itemDirectoryPath = Path.GetDirectoryName(itemPath);
- if (itemDirectoryPath is null)
- {
- return null;
- }
-
- // Ensure the directory path exists
- if (!Directory.Exists(itemDirectoryPath))
- {
- return null;
- }
-
- foreach (var lyricFilePath in Directory.GetFiles(itemDirectoryPath, $"{Path.GetFileNameWithoutExtension(itemPath)}.*"))
- {
- if (lyricProvider.SupportedMediaTypes.Contains(Path.GetExtension(lyricFilePath.AsSpan())[1..], StringComparison.OrdinalIgnoreCase))
- {
- return lyricFilePath;
- }
- }
-
- return null;
- }
-}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 70018bb36..14417a5e4 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -23,7 +23,7 @@ using Microsoft.Extensions.Configuration;
namespace MediaBrowser.Controller.MediaEncoding
{
- public class EncodingHelper
+ public partial class EncodingHelper
{
private const string QsvAlias = "qs";
private const string VaapiAlias = "va";
@@ -37,7 +37,8 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly IMediaEncoder _mediaEncoder;
private readonly ISubtitleEncoder _subtitleEncoder;
private readonly IConfiguration _config;
- private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15);
+ private readonly IConfigurationManager _configurationManager;
+
// i915 hang was fixed by linux 6.2 (3f882f2)
private readonly Version _minKerneli915Hang = new Version(5, 18);
private readonly Version _maxKerneli915Hang = new Version(6, 1, 3);
@@ -45,6 +46,9 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
+ private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
+ private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
+ private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0);
private static readonly string[] _videoProfilesH264 = new[]
{
@@ -64,6 +68,13 @@ namespace MediaBrowser.Controller.MediaEncoding
"Main10"
};
+ private static readonly string[] _videoProfilesAv1 = new[]
+ {
+ "Main",
+ "High",
+ "Professional",
+ };
+
private static readonly HashSet<string> _mp4ContainerNames = new(StringComparer.OrdinalIgnoreCase)
{
"mp4",
@@ -103,21 +114,29 @@ namespace MediaBrowser.Controller.MediaEncoding
IApplicationPaths appPaths,
IMediaEncoder mediaEncoder,
ISubtitleEncoder subtitleEncoder,
- IConfiguration config)
+ IConfiguration config,
+ IConfigurationManager configurationManager)
{
_appPaths = appPaths;
_mediaEncoder = mediaEncoder;
_subtitleEncoder = subtitleEncoder;
_config = config;
+ _configurationManager = configurationManager;
}
+ [GeneratedRegex(@"\s+")]
+ private static partial Regex WhiteSpaceRegex();
+
public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
- => GetH264OrH265Encoder("libx264", "h264", state, encodingOptions);
+ => GetH26xOrAv1Encoder("libx264", "h264", state, encodingOptions);
public string GetH265Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
- => GetH264OrH265Encoder("libx265", "hevc", state, encodingOptions);
+ => GetH26xOrAv1Encoder("libx265", "hevc", state, encodingOptions);
- private string GetH264OrH265Encoder(string defaultEncoder, string hwEncoder, EncodingJobInfo state, EncodingOptions encodingOptions)
+ public string GetAv1Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
+ => GetH26xOrAv1Encoder("libsvtav1", "av1", state, encodingOptions);
+
+ private string GetH26xOrAv1Encoder(string defaultEncoder, string hwEncoder, EncodingJobInfo state, EncodingOptions encodingOptions)
{
// Only use alternative encoders for video files.
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
@@ -162,7 +181,8 @@ namespace MediaBrowser.Controller.MediaEncoding
private bool IsVaapiFullSupported()
{
- return _mediaEncoder.SupportsHwaccel("vaapi")
+ return _mediaEncoder.SupportsHwaccel("drm")
+ && _mediaEncoder.SupportsHwaccel("vaapi")
&& _mediaEncoder.SupportsFilter("scale_vaapi")
&& _mediaEncoder.SupportsFilter("deinterlace_vaapi")
&& _mediaEncoder.SupportsFilter("tonemap_vaapi")
@@ -207,8 +227,8 @@ namespace MediaBrowser.Controller.MediaEncoding
}
if (string.Equals(state.VideoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
- && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
- && string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase))
+ && state.VideoStream.VideoRange == VideoRange.HDR
+ && state.VideoStream.VideoRangeType == VideoRangeType.DOVI)
{
// Only native SW decoder and HW accelerator can parse dovi rpu.
var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
@@ -219,9 +239,9 @@ namespace MediaBrowser.Controller.MediaEncoding
return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder;
}
- return string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
- && (string.Equals(state.VideoStream.VideoRangeType, "HDR10", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.VideoStream.VideoRangeType, "HLG", StringComparison.OrdinalIgnoreCase));
+ return state.VideoStream.VideoRange == VideoRange.HDR
+ && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10
+ || state.VideoStream.VideoRangeType == VideoRangeType.HLG);
}
private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
@@ -233,7 +253,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// libplacebo has partial Dolby Vision to SDR tonemapping support.
return options.EnableTonemapping
- && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
+ && state.VideoStream.VideoRange == VideoRange.HDR
&& GetVideoColorBitDepth(state) == 10;
}
@@ -248,8 +268,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// Native VPP tonemapping may come to QSV in the future.
- return string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
- && string.Equals(state.VideoStream.VideoRangeType, "HDR10", StringComparison.OrdinalIgnoreCase);
+ return state.VideoStream.VideoRange == VideoRange.HDR
+ && state.VideoStream.VideoRangeType == VideoRangeType.HDR10;
}
/// <summary>
@@ -264,6 +284,11 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(codec))
{
+ if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetAv1Encoder(state, encodingOptions);
+ }
+
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{
@@ -563,6 +588,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return Array.FindIndex(_videoProfilesH265, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase));
}
+ if (string.Equals("av1", videoCodec, StringComparison.OrdinalIgnoreCase))
+ {
+ return Array.FindIndex(_videoProfilesAv1, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase));
+ }
+
return -1;
}
@@ -712,28 +742,43 @@ namespace MediaBrowser.Controller.MediaEncoding
options);
}
- private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string alias)
+ private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string srcDeviceAlias, string alias)
{
alias ??= VaapiAlias;
renderNodePath = renderNodePath ?? "/dev/dri/renderD128";
- var options = string.IsNullOrEmpty(driver)
- ? renderNodePath
- : ",driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver);
+ var driverOpts = string.IsNullOrEmpty(driver)
+ ? ":" + renderNodePath
+ : ":,driver=" + driver + (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver);
+ var options = string.IsNullOrEmpty(srcDeviceAlias)
+ ? driverOpts
+ : "@" + srcDeviceAlias;
return string.Format(
CultureInfo.InvariantCulture,
- " -init_hw_device vaapi={0}:{1}",
+ " -init_hw_device vaapi={0}{1}",
alias,
options);
}
+ private string GetDrmDeviceArgs(string renderNodePath, string alias)
+ {
+ alias ??= DrmAlias;
+ renderNodePath = renderNodePath ?? "/dev/dri/renderD128";
+
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ " -init_hw_device drm={0}:{1}",
+ alias,
+ renderNodePath);
+ }
+
private string GetQsvDeviceArgs(string alias)
{
var arg = " -init_hw_device qsv=" + (alias ?? QsvAlias);
if (OperatingSystem.IsLinux())
{
// derive qsv from vaapi device
- return GetVaapiDeviceArgs(null, "iHD", "i915", VaapiAlias) + arg + "@" + VaapiAlias;
+ return GetVaapiDeviceArgs(null, "iHD", "i915", null, VaapiAlias) + arg + "@" + VaapiAlias;
}
if (OperatingSystem.IsWindows())
@@ -754,9 +799,12 @@ namespace MediaBrowser.Controller.MediaEncoding
public string GetGraphicalSubCanvasSize(EncodingJobInfo state)
{
+ // DVBSUB and DVDSUB use the fixed canvas size 720x576
if (state.SubtitleStream is not null
&& state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode
- && !state.SubtitleStream.IsTextSubtitleStream)
+ && !state.SubtitleStream.IsTextSubtitleStream
+ && !string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(state.SubtitleStream.Codec, "DVDSUB", StringComparison.OrdinalIgnoreCase))
{
var inW = state.VideoStream?.Width;
var inH = state.VideoStream?.Height;
@@ -824,21 +872,17 @@ namespace MediaBrowser.Controller.MediaEncoding
if (_mediaEncoder.IsVaapiDeviceInteliHD)
{
- args.Append(GetVaapiDeviceArgs(null, "iHD", null, VaapiAlias));
+ args.Append(GetVaapiDeviceArgs(null, "iHD", null, null, VaapiAlias));
}
else if (_mediaEncoder.IsVaapiDeviceInteli965)
{
// Only override i965 since it has lower priority than iHD in libva lookup.
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME", "i965");
Environment.SetEnvironmentVariable("LIBVA_DRIVER_NAME_JELLYFIN", "i965");
- args.Append(GetVaapiDeviceArgs(null, "i965", null, VaapiAlias));
- }
- else
- {
- args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, VaapiAlias));
+ args.Append(GetVaapiDeviceArgs(null, "i965", null, null, VaapiAlias));
}
- var filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias);
+ var filterDevArgs = string.Empty;
var doOclTonemap = isHwTonemapAvailable && IsOpenclFullSupported();
if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965)
@@ -851,19 +895,30 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (_mediaEncoder.IsVaapiDeviceAmd)
{
+ // Disable AMD EFC feature since it's still unstable in upstream Mesa.
+ Environment.SetEnvironmentVariable("AMD_DEBUG", "noefc");
+
if (IsVulkanFullSupported()
- && _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier
- && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
+ && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop)
{
+ args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias));
+ args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias));
+ args.Append(GetVulkanDeviceArgs(0, null, DrmAlias, VulkanAlias));
+
// libplacebo wants an explicitly set vulkan filter device.
- args.Append(GetVulkanDeviceArgs(0, null, VaapiAlias, VulkanAlias));
filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias);
}
- else if (doOclTonemap)
+ else
{
- // ROCm/ROCr OpenCL runtime
- args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias));
- filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
+ args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, null, VaapiAlias));
+ filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias);
+
+ if (doOclTonemap)
+ {
+ // ROCm/ROCr OpenCL runtime
+ args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias));
+ filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
+ }
}
}
else if (doOclTonemap)
@@ -1007,7 +1062,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
{
- var tmpConcatPath = Path.Join(options.TranscodingTempPath, state.MediaSource.Id + ".concat");
+ var tmpConcatPath = Path.Join(_configurationManager.GetTranscodePath(), state.MediaSource.Id + ".concat");
_mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath);
arg.Append(" -f concat -safe 0 -i ")
.Append(tmpConcatPath);
@@ -1162,6 +1217,12 @@ namespace MediaBrowser.Controller.MediaEncoding
int bitrate = state.OutputVideoBitrate.Value;
+ // Bit rate under 1000k is not allowed in h264_qsv
+ if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
+ {
+ bitrate = Math.Max(bitrate, 1000);
+ }
+
// Currently use the same buffer size for all encoders
int bufsize = bitrate * 2;
@@ -1179,6 +1240,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return FormattableString.Invariant($" -b:v {bitrate}");
}
+ if (string.Equals(videoCodec, "libsvtav1", StringComparison.OrdinalIgnoreCase))
+ {
+ return FormattableString.Invariant($" -b:v {bitrate} -bufsize {bufsize}");
+ }
+
if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase))
{
@@ -1186,14 +1252,16 @@ namespace MediaBrowser.Controller.MediaEncoding
}
if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "av1_amf", StringComparison.OrdinalIgnoreCase))
{
// Override the too high default qmin 18 in transcoding preset
return FormattableString.Invariant($" -rc cbr -qmin 0 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
}
if (string.Equals(videoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoCodec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoCodec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoCodec, "av1_vaapi", StringComparison.OrdinalIgnoreCase))
{
// VBR in i965 driver may result in pixelated output.
if (_mediaEncoder.IsVaapiDeviceInteli965)
@@ -1211,14 +1279,23 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
{
- if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
+ {
+ // Transcode to level 5.3 (15) and lower for maximum compatibility.
+ // https://en.wikipedia.org/wiki/AV1#Levels
+ if (requestLevel < 0 || requestLevel >= 15)
+ {
+ return "15";
+ }
+ }
+ else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
{
// Transcode to level 5.0 and lower for maximum compatibility.
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
- if (requestLevel >= 150)
+ if (requestLevel < 0 || requestLevel >= 150)
{
return "150";
}
@@ -1228,7 +1305,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Transcode to level 5.1 and lower for maximum compatibility.
// h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4.
// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
- if (requestLevel >= 51)
+ if (requestLevel < 0 || requestLevel >= 51)
{
return "51";
}
@@ -1340,22 +1417,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var args = string.Empty;
var gopArg = string.Empty;
- var keyFrameArg = string.Empty;
- if (isEventPlaylist)
- {
- keyFrameArg = string.Format(
- CultureInfo.InvariantCulture,
- " -force_key_frames:0 \"expr:gte(t,n_forced*{0})\"",
- segmentLength);
- }
- else if (startNumber.HasValue)
- {
- keyFrameArg = string.Format(
- CultureInfo.InvariantCulture,
- " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
- startNumber.Value * segmentLength,
- segmentLength);
- }
+
+ var keyFrameArg = string.Format(
+ CultureInfo.InvariantCulture,
+ " -force_key_frames:0 \"expr:gte(t,n_forced*{0})\"",
+ segmentLength);
var framerate = state.VideoStream?.RealFrameRate;
if (framerate.HasValue)
@@ -1377,14 +1443,18 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(codec, "av1_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "av1_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "av1_amf", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "libsvtav1", StringComparison.OrdinalIgnoreCase))
{
args += gopArg;
}
else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(codec, "av1_vaapi", StringComparison.OrdinalIgnoreCase))
{
args += keyFrameArg;
@@ -1399,6 +1469,13 @@ namespace MediaBrowser.Controller.MediaEncoding
args += keyFrameArg + gopArg;
}
+ // global_header produced by AMD HEVC VA-API encoder causes non-playable fMP4 on iOS
+ if (string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
+ && _mediaEncoder.IsVaapiDeviceAmd)
+ {
+ args += " -flags:v -global_header";
+ }
+
return args;
}
@@ -1520,18 +1597,60 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -crf " + defaultCrf;
}
}
+ else if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase))
+ {
+ // Default to use the recommended preset 10.
+ // Omit presets < 5, which are too slow for on the fly encoding.
+ // https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Ffmpeg.md
+ param += encodingOptions.EncoderPreset switch
+ {
+ "veryslow" => " -preset 5",
+ "slower" => " -preset 6",
+ "slow" => " -preset 7",
+ "medium" => " -preset 8",
+ "fast" => " -preset 9",
+ "faster" => " -preset 10",
+ "veryfast" => " -preset 11",
+ "superfast" => " -preset 12",
+ "ultrafast" => " -preset 13",
+ _ => " -preset 10"
+ };
+ }
+ else if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "av1_vaapi", StringComparison.OrdinalIgnoreCase))
+ {
+ // -compression_level is not reliable on AMD.
+ if (_mediaEncoder.IsVaapiDeviceInteliHD)
+ {
+ param += encodingOptions.EncoderPreset switch
+ {
+ "veryslow" => " -compression_level 1",
+ "slower" => " -compression_level 2",
+ "slow" => " -compression_level 3",
+ "medium" => " -compression_level 4",
+ "fast" => " -compression_level 5",
+ "faster" => " -compression_level 6",
+ "veryfast" => " -compression_level 7",
+ "superfast" => " -compression_level 7",
+ "ultrafast" => " -compression_level 7",
+ _ => string.Empty
+ };
+ }
+ }
else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv)
- || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_qsv)
+ || string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase) // hevc (hevc_qsv)
+ || string.Equals(videoEncoder, "av1_qsv", StringComparison.OrdinalIgnoreCase)) // av1 (av1_qsv)
{
- string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
+ string[] valid_presets = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
- if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparison.OrdinalIgnoreCase))
+ if (valid_presets.Contains(encodingOptions.EncoderPreset, StringComparison.OrdinalIgnoreCase))
{
param += " -preset " + encodingOptions.EncoderPreset;
}
else
{
- param += " -preset 7";
+ param += " -preset veryfast";
}
// Only h264_qsv has look_ahead option
@@ -1541,7 +1660,8 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
- || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc)
+ || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) // hevc (hevc_nvenc)
+ || string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase)) // av1 (av1_nvenc)
{
switch (encodingOptions.EncoderPreset)
{
@@ -1549,11 +1669,11 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -preset p7";
break;
- case "slow":
+ case "slower":
param += " -preset p6";
break;
- case "slower":
+ case "slow":
param += " -preset p5";
break;
@@ -1581,13 +1701,14 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf)
- || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_amf)
+ || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) // hevc (hevc_amf)
+ || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase)) // av1 (av1_amf)
{
switch (encodingOptions.EncoderPreset)
{
case "veryslow":
- case "slow":
case "slower":
+ case "slow":
param += " -quality quality";
break;
@@ -1608,9 +1729,15 @@ namespace MediaBrowser.Controller.MediaEncoding
break;
}
+ if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase))
+ {
+ param += " -header_insertion_mode gop";
+ }
+
if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
{
- param += " -header_insertion_mode gop -gops_per_idr 1";
+ param += " -gops_per_idr 1";
}
}
else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // vp8
@@ -1719,7 +1846,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault() ?? string.Empty;
- profile = Regex.Replace(profile, @"\s+", string.Empty);
+ profile = WhiteSpaceRegex().Replace(profile, string.Empty);
// We only transcode to HEVC 8-bit for now, force Main Profile.
if (profile.Contains("main10", StringComparison.OrdinalIgnoreCase)
@@ -1741,6 +1868,14 @@ namespace MediaBrowser.Controller.MediaEncoding
profile = "high";
}
+ // We only need Main profile of AV1 encoders.
+ if (videoEncoder.Contains("av1", StringComparison.OrdinalIgnoreCase)
+ && (profile.Contains("high", StringComparison.OrdinalIgnoreCase)
+ || profile.Contains("professional", StringComparison.OrdinalIgnoreCase)))
+ {
+ profile = "main";
+ }
+
// h264_vaapi does not support Baseline profile, force Constrained Baseline in this case,
// which is compatible (and ugly).
if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
@@ -1782,7 +1917,9 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(profile))
{
- if (!string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
+ // Currently there's no profile option in av1_nvenc encoder
+ if (!(string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)))
{
param += " -profile:v:0 " + profile;
}
@@ -1808,19 +1945,41 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -level " + (hevcLevel / 3);
}
}
+ else if (string.Equals(videoEncoder, "av1_qsv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase))
+ {
+ // libsvtav1 and av1_qsv use -level 60 instead of -level 16
+ // https://aomedia.org/av1/specification/annex-a/
+ if (int.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out int av1Level))
+ {
+ var x = 2 + (av1Level >> 2);
+ var y = av1Level & 3;
+ var res = (x * 10) + y;
+ param += " -level " + res;
+ }
+ }
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase))
{
param += " -level " + level;
}
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase))
{
// level option may cause NVENC to fail.
// NVENC cannot adjust the given level, just throw an error.
+ }
+ else if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "av1_vaapi", StringComparison.OrdinalIgnoreCase))
+ {
// level option may cause corrupted frames on AMD VAAPI.
+ if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965)
+ {
+ param += " -level " + level;
+ }
}
else if (!string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
{
@@ -1842,6 +2001,20 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -x265-params:0 no-info=1";
}
+ if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)
+ && _mediaEncoder.EncoderVersion >= _minFFmpegSvtAv1Params)
+ {
+ param += " -svtav1-params:0 rc=1:tune=0:film-grain=0:enable-overlays=1:enable-tf=0";
+ }
+
+ /* Access unit too large: 8192 < 20880 error */
+ if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) &&
+ _mediaEncoder.EncoderVersion >= _minFFmpegVaapiH26xEncA53CcSei)
+ {
+ param += " -sei -a53_cc";
+ }
+
return param;
}
@@ -1920,12 +2093,12 @@ namespace MediaBrowser.Controller.MediaEncoding
var requestedRangeTypes = state.GetRequestedRangeTypes(videoStream.Codec);
if (requestedRangeTypes.Length > 0)
{
- if (string.IsNullOrEmpty(videoStream.VideoRangeType))
+ if (videoStream.VideoRangeType == VideoRangeType.Unknown)
{
return false;
}
- if (!requestedRangeTypes.Contains(videoStream.VideoRangeType, StringComparison.OrdinalIgnoreCase))
+ if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase))
{
return false;
}
@@ -2148,7 +2321,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec);
// Don't scale the real bitrate lower than the requested bitrate
- var scaleFactor = Math.Min(outputScaleFactor / inputScaleFactor, 1);
+ var scaleFactor = Math.Max(outputScaleFactor / inputScaleFactor, 1);
if (bitrate <= 500000)
{
@@ -2539,7 +2712,7 @@ namespace MediaBrowser.Controller.MediaEncoding
string args = string.Empty;
// http://ffmpeg.org/ffmpeg-all.html#toc-Complex-filtergraphs-1
- if (state.VideoStream != null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal))
+ if (state.VideoStream is not null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal))
{
int videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream);
@@ -2929,7 +3102,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Empty;
}
- public static string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat)
+ public string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat)
{
if (string.IsNullOrEmpty(hwTonemapSuffix))
{
@@ -2941,7 +3114,8 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase))
{
- args = "tonemap_vaapi=format={0}:p=bt709:t=bt709:m=bt709,procamp_vaapi=b={1}:c={2}:extra_hw_frames=16";
+ args = "procamp_vaapi=b={1}:c={2},tonemap_vaapi=format={0}:p=bt709:t=bt709:m=bt709:extra_hw_frames=32";
+
return string.Format(
CultureInfo.InvariantCulture,
args,
@@ -2949,37 +3123,28 @@ namespace MediaBrowser.Controller.MediaEncoding
options.VppTonemappingBrightness,
options.VppTonemappingContrast);
}
-
- if (string.Equals(hwTonemapSuffix, "vulkan", StringComparison.OrdinalIgnoreCase))
+ else
{
- args = "libplacebo=format={1}:tonemapping={2}:color_primaries=bt709:color_trc=bt709:colorspace=bt709:peak_detect=0:upscaler=none:downscaler=none";
-
- if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase))
- {
- args += ":range={6}";
- }
+ args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}";
- if (string.Equals(options.TonemappingAlgorithm, "bt2390", StringComparison.OrdinalIgnoreCase))
- {
- algorithm = "bt.2390";
- }
- else if (string.Equals(options.TonemappingAlgorithm, "none", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(options.TonemappingMode, "max", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(options.TonemappingMode, "rgb", StringComparison.OrdinalIgnoreCase))
{
- algorithm = "clip";
+ if (_mediaEncoder.EncoderVersion >= _minFFmpegOclCuTonemapMode)
+ {
+ args += ":tonemap_mode={5}";
+ }
}
- }
- else
- {
- args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}";
if (options.TonemappingParam != 0)
{
- args += ":param={5}";
+ args += ":param={6}";
}
- if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(options.TonemappingRange, "tv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(options.TonemappingRange, "pc", StringComparison.OrdinalIgnoreCase))
{
- args += ":range={6}";
+ args += ":range={7}";
}
}
@@ -2991,10 +3156,80 @@ namespace MediaBrowser.Controller.MediaEncoding
algorithm,
options.TonemappingPeak,
options.TonemappingDesat,
+ options.TonemappingMode,
options.TonemappingParam,
options.TonemappingRange);
}
+ public string GetLibplaceboFilter(
+ EncodingOptions options,
+ string videoFormat,
+ bool doTonemap,
+ int? videoWidth,
+ int? videoHeight,
+ int? requestedWidth,
+ int? requestedHeight,
+ int? requestedMaxWidth,
+ int? requestedMaxHeight)
+ {
+ var (outWidth, outHeight) = GetFixedOutputSize(
+ videoWidth,
+ videoHeight,
+ requestedWidth,
+ requestedHeight,
+ requestedMaxWidth,
+ requestedMaxHeight);
+
+ var isFormatFixed = !string.IsNullOrEmpty(videoFormat);
+ var isSizeFixed = !videoWidth.HasValue
+ || outWidth.Value != videoWidth.Value
+ || !videoHeight.HasValue
+ || outHeight.Value != videoHeight.Value;
+
+ var sizeArg = isSizeFixed ? (":w=" + outWidth.Value + ":h=" + outHeight.Value) : string.Empty;
+ var formatArg = isFormatFixed ? (":format=" + videoFormat) : string.Empty;
+ var tonemapArg = string.Empty;
+
+ if (doTonemap)
+ {
+ var algorithm = options.TonemappingAlgorithm;
+ var mode = options.TonemappingMode;
+ var range = options.TonemappingRange;
+
+ if (string.Equals(algorithm, "bt2390", StringComparison.OrdinalIgnoreCase))
+ {
+ algorithm = "bt.2390";
+ }
+ else if (string.Equals(algorithm, "none", StringComparison.OrdinalIgnoreCase))
+ {
+ algorithm = "clip";
+ }
+
+ tonemapArg = ":tonemapping=" + algorithm;
+
+ if (string.Equals(mode, "max", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(mode, "rgb", StringComparison.OrdinalIgnoreCase))
+ {
+ tonemapArg += ":tonemapping_mode=" + mode;
+ }
+
+ tonemapArg += ":peak_detect=0:color_primaries=bt709:color_trc=bt709:colorspace=bt709";
+
+ if (string.Equals(range, "tv", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(range, "pc", StringComparison.OrdinalIgnoreCase))
+ {
+ tonemapArg += ":range=" + range;
+ }
+ }
+
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ "libplacebo=upscaler=none:downscaler=none{0}{1}{2}",
+ sizeArg,
+ formatArg,
+ tonemapArg);
+ }
+
/// <summary>
/// Gets the parameter of software filter chain.
/// </summary>
@@ -3569,7 +3804,7 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add(swDeintFilter);
}
- var outFormat = doOclTonemap ? "yuv420p10le" : "yuv420p";
+ var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12");
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
// sw scale
mainFilters.Add(swScaleFilter);
@@ -3596,12 +3831,6 @@ namespace MediaBrowser.Controller.MediaEncoding
// map from d3d11va to qsv.
mainFilters.Add("hwmap=derive_device=qsv");
}
- else
- {
- // Insert a qsv scaler to sync the decoder surface,
- // msdk will passthrough this internally.
- mainFilters.Add("hwmap=derive_device=qsv,scale_qsv");
- }
}
// hw deint
@@ -3770,7 +3999,7 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add(swDeintFilter);
}
- var outFormat = doOclTonemap ? "yuv420p10le" : "yuv420p";
+ var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12");
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
// sw scale
mainFilters.Add(swScaleFilter);
@@ -3992,14 +4221,13 @@ namespace MediaBrowser.Controller.MediaEncoding
// prefered vaapi + vulkan filters pipeline
if (_mediaEncoder.IsVaapiDeviceAmd
&& isVaapiVkSupported
- && _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier
- && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
+ && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop)
{
- // AMD radeonsi path(Vega/gfx9+, kernel>=5.15), with extra vulkan tonemap and overlay support.
+ // AMD radeonsi path(targeting Polaris/gfx8+), with extra vulkan tonemap and overlay support.
return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
}
- // Intel i965 and Amd radeonsi/r600 path(Polaris/gfx8-), only featuring scale and deinterlace support.
+ // Intel i965 and Amd legacy driver path, only featuring scale and deinterlace support.
return GetVaapiLimitedVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
}
@@ -4224,7 +4452,6 @@ namespace MediaBrowser.Controller.MediaEncoding
var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
var isSwEncoder = !isVaapiEncoder;
- var isVaInVaOut = isVaapiDecoder && isVaapiEncoder;
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
@@ -4253,99 +4480,79 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add(swDeintFilter);
}
- var outFormat = doVkTonemap ? "yuv420p10le" : "nv12";
- var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
- // sw scale
- mainFilters.Add(swScaleFilter);
- mainFilters.Add("format=" + outFormat);
-
- // keep video at memory except vk tonemap,
- // since the overhead caused by hwupload >>> using sw filter.
- // sw => hw
- if (doVkTonemap)
+ if (doVkTonemap || hasSubs)
{
- mainFilters.Add("hwupload=derive_device=vaapi");
- mainFilters.Add("format=vaapi");
- mainFilters.Add("hwmap=derive_device=vulkan");
+ // sw => hw
+ mainFilters.Add("hwupload=derive_device=vulkan");
mainFilters.Add("format=vulkan");
}
+ else
+ {
+ // sw scale
+ var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
+ mainFilters.Add(swScaleFilter);
+ mainFilters.Add("format=nv12");
+ }
}
else if (isVaapiDecoder)
{
// INPUT vaapi surface(vram)
- // hw deint
- if (doDeintH2645)
+ if (doVkTonemap || hasSubs)
{
- var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi");
- mainFilters.Add(deintFilter);
+ // map from vaapi to vulkan/drm via interop (Polaris/gfx8+).
+ mainFilters.Add("hwmap=derive_device=vulkan");
+ mainFilters.Add("format=vulkan");
}
-
- var outFormat = doVkTonemap ? string.Empty : (hasSubs && isVaInVaOut ? "bgra" : "nv12");
- var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
-
- // allocate extra pool sizes for overlay_vulkan
- if (!string.IsNullOrEmpty(hwScaleFilter) && isVaInVaOut && hasSubs)
+ else
{
- hwScaleFilter += ":extra_hw_frames=32";
- }
-
- // hw scale
- mainFilters.Add(hwScaleFilter);
- }
+ // hw deint
+ if (doDeintH2645)
+ {
+ var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi");
+ mainFilters.Add(deintFilter);
+ }
- if ((isVaapiDecoder && doVkTonemap) || (isVaInVaOut && (doVkTonemap || hasSubs)))
- {
- // map from vaapi to vulkan via vaapi-vulkan interop (Vega/gfx9+).
- mainFilters.Add("hwmap=derive_device=vulkan");
- mainFilters.Add("format=vulkan");
+ // hw scale
+ var hwScaleFilter = GetHwScaleFilter("vaapi", "nv12", inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ mainFilters.Add(hwScaleFilter);
+ }
}
- // vk tonemap
- if (doVkTonemap)
+ // vk libplacebo
+ if (doVkTonemap || hasSubs)
{
- var outFormat = isVaInVaOut && hasSubs ? "bgra" : "nv12";
- var tonemapFilter = GetHwTonemapFilter(options, "vulkan", outFormat);
- mainFilters.Add(tonemapFilter);
+ var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ mainFilters.Add(libplaceboFilter);
}
- if (doVkTonemap && isVaInVaOut && !hasSubs)
+ if (doVkTonemap && !hasSubs)
{
- // OUTPUT vaapi(nv12/bgra) surface(vram)
- // reverse-mapping via vaapi-vulkan interop.
- mainFilters.Add("hwmap=derive_device=vaapi:reverse=1");
+ // OUTPUT vaapi(nv12) surface(vram)
+ // map from vulkan/drm to vaapi via interop (Polaris/gfx8+).
+ mainFilters.Add("hwmap=derive_device=vaapi");
mainFilters.Add("format=vaapi");
- }
- var memoryOutput = false;
- var isUploadForVkTonemap = isSwDecoder && doVkTonemap;
- if ((isVaapiDecoder && isSwEncoder) || isUploadForVkTonemap)
- {
- memoryOutput = true;
+ // clear the surf->meta_offset and output nv12
+ mainFilters.Add("scale_vaapi=format=nv12");
- // OUTPUT nv12 surface(memory)
- mainFilters.Add("hwdownload");
- mainFilters.Add("format=nv12");
- }
-
- // OUTPUT nv12 surface(memory)
- if (isSwDecoder && isVaapiEncoder)
- {
- memoryOutput = true;
+ // hw deint
+ if (doDeintH2645)
+ {
+ var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi");
+ mainFilters.Add(deintFilter);
+ }
}
- if (memoryOutput)
+ if (!hasSubs)
{
- // text subtitles
- if (hasTextSubs)
+ // OUTPUT nv12 surface(memory)
+ if (isSwEncoder && (doVkTonemap || isVaapiDecoder))
{
- var textSubtitlesFilter = GetTextSubtitlesFilter(state, false, false);
- mainFilters.Add(textSubtitlesFilter);
+ mainFilters.Add("hwdownload");
+ mainFilters.Add("format=nv12");
}
- }
- if (memoryOutput && isVaapiEncoder)
- {
- if (!hasGraphicalSubs)
+ if (isSwDecoder && isVaapiEncoder && !doVkTonemap)
{
mainFilters.Add("hwupload_vaapi");
}
@@ -4354,55 +4561,51 @@ namespace MediaBrowser.Controller.MediaEncoding
/* Make sub and overlay filters for subtitle stream */
var subFilters = new List<string>();
var overlayFilters = new List<string>();
- if (isVaInVaOut)
+ if (hasSubs)
{
- if (hasSubs)
+ if (hasGraphicalSubs)
{
- if (hasGraphicalSubs)
- {
- // scale=s=1280x720,format=bgra,hwupload
- var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
- subFilters.Add(subSwScaleFilter);
- subFilters.Add("format=bgra");
- }
- else if (hasTextSubs)
- {
- var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, hasAssSubs ? 10 : 5);
- var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
- subFilters.Add(alphaSrcFilter);
- subFilters.Add("format=bgra");
- subFilters.Add(subTextSubtitlesFilter);
- }
-
- // prefer vaapi hwupload to vulkan hwupload,
- // Mesa RADV does not support a dedicated transfer queue.
- subFilters.Add("hwupload=derive_device=vaapi");
- subFilters.Add("format=vaapi");
- subFilters.Add("hwmap=derive_device=vulkan");
- subFilters.Add("format=vulkan");
+ // scale=s=1280x720,format=bgra,hwupload
+ var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+ subFilters.Add(subSwScaleFilter);
+ subFilters.Add("format=bgra");
+ }
+ else if (hasTextSubs)
+ {
+ var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, hasAssSubs ? 10 : 5);
+ var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
+ subFilters.Add(alphaSrcFilter);
+ subFilters.Add("format=bgra");
+ subFilters.Add(subTextSubtitlesFilter);
+ }
- overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0");
+ subFilters.Add("hwupload=derive_device=vulkan");
+ subFilters.Add("format=vulkan");
- // TODO: figure out why libplacebo can sync without vaSyncSurface VPP support in radeonsi.
- overlayFilters.Add("libplacebo=format=nv12:apply_filmgrain=0:apply_dolbyvision=0:upscaler=none:downscaler=none:dithering=none");
+ overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0");
- // OUTPUT vaapi(nv12/bgra) surface(vram)
- // reverse-mapping via vaapi-vulkan interop.
- overlayFilters.Add("hwmap=derive_device=vaapi:reverse=1");
- overlayFilters.Add("format=vaapi");
+ if (isSwEncoder)
+ {
+ // OUTPUT nv12 surface(memory)
+ overlayFilters.Add("scale_vulkan=format=nv12");
+ overlayFilters.Add("hwdownload");
+ overlayFilters.Add("format=nv12");
}
- }
- else if (memoryOutput)
- {
- if (hasGraphicalSubs)
+ else if (isVaapiEncoder)
{
- var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
- subFilters.Add(subSwScaleFilter);
- overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
+ // OUTPUT vaapi(nv12) surface(vram)
+ // map from vulkan/drm to vaapi via interop (Polaris/gfx8+).
+ overlayFilters.Add("hwmap=derive_device=vaapi");
+ overlayFilters.Add("format=vaapi");
- if (isVaapiEncoder)
+ // clear the surf->meta_offset and output nv12
+ overlayFilters.Add("scale_vaapi=format=nv12");
+
+ // hw deint
+ if (doDeintH2645)
{
- overlayFilters.Add("hwupload_vaapi");
+ var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi");
+ overlayFilters.Add(deintFilter);
}
}
}
@@ -5052,10 +5255,8 @@ namespace MediaBrowser.Controller.MediaEncoding
if (isD3d11Supported && isCodecAvailable)
{
- // set -threads 3 to intel d3d11va decoder explicitly. Lower threads may result in dead lock.
- // on newer devices such as Xe, the larger the init_pool_size, the longer the initialization time for opencl to derive from d3d11.
return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty)
- + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty);
+ + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + " -threads 2" + (isAv1 ? " -c:v av1" : string.Empty);
}
}
else
@@ -5489,7 +5690,6 @@ namespace MediaBrowser.Controller.MediaEncoding
// Apply -analyzeduration as per the environment variable,
// otherwise ffmpeg will break on certain files due to default value is 0.
- // The default value of -probesize is more than enough, so leave it as is.
var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
if (state.MediaSource.AnalyzeDurationMs > 0)
@@ -5508,6 +5708,14 @@ namespace MediaBrowser.Controller.MediaEncoding
inputModifier = inputModifier.Trim();
+ // Apply -probesize if configured
+ var ffmpegProbeSize = _config.GetFFmpegProbeSize();
+
+ if (!string.IsNullOrEmpty(ffmpegProbeSize))
+ {
+ inputModifier += $" -probesize {ffmpegProbeSize}";
+ }
+
var userAgentParam = GetUserAgentParam(state);
if (!string.IsNullOrEmpty(userAgentParam))
@@ -5755,19 +5963,25 @@ namespace MediaBrowser.Controller.MediaEncoding
private void ShiftVideoCodecsIfNeeded(List<string> videoCodecs, EncodingOptions encodingOptions)
{
- // Shift hevc/h265 to the end of list if hevc encoding is not allowed.
- if (encodingOptions.AllowHevcEncoding)
+ // No need to shift if there is only one supported video codec.
+ if (videoCodecs.Count < 2)
{
return;
}
- // No need to shift if there is only one supported video codec.
- if (videoCodecs.Count < 2)
+ // Shift codecs to the end of list if it's not allowed.
+ var shiftVideoCodecs = new List<string>();
+ if (!encodingOptions.AllowHevcEncoding)
{
- return;
+ shiftVideoCodecs.Add("hevc");
+ shiftVideoCodecs.Add("h265");
+ }
+
+ if (!encodingOptions.AllowAv1Encoding)
+ {
+ shiftVideoCodecs.Add("av1");
}
- var shiftVideoCodecs = new[] { "hevc", "h265" };
if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
return;
@@ -6035,6 +6249,12 @@ namespace MediaBrowser.Controller.MediaEncoding
audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state));
}
+ if (GetAudioEncoder(state).StartsWith("pcm_", StringComparison.Ordinal))
+ {
+ audioTranscodeParams.Add(string.Concat("-f ", GetAudioEncoder(state).AsSpan(4)));
+ audioTranscodeParams.Add("-ar " + state.BaseRequest.AudioBitRate);
+ }
+
if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase))
{
// opus only supports specific sampling rates
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index a6b541660..17813559a 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
@@ -367,22 +368,21 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Gets the target video range type.
/// </summary>
- public string TargetVideoRangeType
+ public VideoRangeType TargetVideoRangeType
{
get
{
if (BaseRequest.Static || EncodingHelper.IsCopyCodec(OutputVideoCodec))
{
- return VideoStream?.VideoRangeType;
+ return VideoStream?.VideoRangeType ?? VideoRangeType.Unknown;
}
- var requestedRangeType = GetRequestedRangeTypes(ActualOutputVideoCodec).FirstOrDefault();
- if (!string.IsNullOrEmpty(requestedRangeType))
+ if (Enum.TryParse(GetRequestedRangeTypes(ActualOutputVideoCodec).FirstOrDefault() ?? "Unknown", true, out VideoRangeType requestedRangeType))
{
return requestedRangeType;
}
- return null;
+ return VideoRangeType.Unknown;
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index f830b9f29..4114dea4f 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -64,8 +64,8 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Gets a value indicating whether the configured Vaapi device supports vulkan drm format modifier.
/// </summary>
- /// <value><c>true</c> if the Vaapi device supports vulkan drm format modifier, <c>false</c> otherwise.</value>
- bool IsVaapiDeviceSupportVulkanFmtModifier { get; }
+ /// <value><c>true</c> if the Vaapi device supports vulkan drm interop, <c>false</c> otherwise.</value>
+ bool IsVaapiDeviceSupportVulkanDrmInterop { get; }
/// <summary>
/// Whether given encoder codec is supported.
diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
index 3b34af4e9..3d288b9f8 100644
--- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
+++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs
@@ -20,12 +20,12 @@ namespace MediaBrowser.Controller.MediaEncoding
_logger = logger;
}
- public async Task StartStreamingLog(EncodingJobInfo state, Stream source, Stream target)
+ public async Task StartStreamingLog(EncodingJobInfo state, StreamReader reader, Stream target)
{
try
{
using (target)
- using (var reader = new StreamReader(source))
+ using (reader)
{
while (!reader.EndOfStream && reader.BaseStream.CanRead)
{
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index 0524999c7..e0942e490 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
@@ -9,7 +9,7 @@ using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Model.Net;
+using MediaBrowser.Controller.Net.WebSocketMessages;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -96,7 +96,7 @@ namespace MediaBrowser.Controller.Net
/// Starts sending messages over a web socket.
/// </summary>
/// <param name="message">The message.</param>
- private void Start(WebSocketMessageInfo message)
+ protected virtual void Start(WebSocketMessageInfo message)
{
var vals = message.Data.Split(',');
@@ -169,9 +169,8 @@ namespace MediaBrowser.Controller.Net
if (data is not null)
{
await connection.SendAsync(
- new WebSocketMessage<TReturnDataType>
+ new OutboundWebSocketMessage<TReturnDataType>
{
- MessageId = Guid.NewGuid(),
MessageType = Type,
Data = data
},
diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
index 4f2492b89..bba5a6b85 100644
--- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs
+++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
@@ -1,14 +1,15 @@
-#pragma warning disable CS1591
-
using System;
using System.Net;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Model.Net;
+using MediaBrowser.Controller.Net.WebSocketMessages;
namespace MediaBrowser.Controller.Net
{
+ /// <summary>
+ /// Interface for WebSocket connections.
+ /// </summary>
public interface IWebSocketConnection : IAsyncDisposable, IDisposable
{
/// <summary>
@@ -41,6 +42,11 @@ namespace MediaBrowser.Controller.Net
WebSocketState State { get; }
/// <summary>
+ /// Gets the authorization information.
+ /// </summary>
+ public AuthorizationInfo AuthorizationInfo { get; }
+
+ /// <summary>
/// Gets the remote end point.
/// </summary>
/// <value>The remote end point.</value>
@@ -49,13 +55,27 @@ namespace MediaBrowser.Controller.Net
/// <summary>
/// Sends a message asynchronously.
/// </summary>
+ /// <param name="message">The message.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="ArgumentNullException">The message is null.</exception>
+ Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Sends a message asynchronously.
+ /// </summary>
/// <typeparam name="T">The type of websocket message data.</typeparam>
/// <param name="message">The message.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
/// <exception cref="ArgumentNullException">The message is null.</exception>
- Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken);
+ Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken);
- Task ProcessAsync(CancellationToken cancellationToken = default);
+ /// <summary>
+ /// Receives a message asynchronously.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ Task ReceiveAsync(CancellationToken cancellationToken = default);
}
}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessage.cs
new file mode 100644
index 000000000..92183e792
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessage.cs
@@ -0,0 +1,22 @@
+using System.Text.Json.Serialization;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net;
+
+/// <summary>
+/// Websocket message without data.
+/// </summary>
+public abstract class WebSocketMessage
+{
+ /// <summary>
+ /// Gets or sets the type of the message.
+ /// TODO make this abstract and get only.
+ /// </summary>
+ public virtual SessionMessageType MessageType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the server id.
+ /// </summary>
+ [JsonIgnore]
+ public string? ServerId { get; set; }
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs
index 6f7ebf156..f7a9ccc44 100644
--- a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs
+++ b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs
@@ -1,13 +1,13 @@
#nullable disable
-using MediaBrowser.Model.Net;
+using MediaBrowser.Controller.Net.WebSocketMessages;
namespace MediaBrowser.Controller.Net
{
/// <summary>
/// Class WebSocketMessageInfo.
/// </summary>
- public class WebSocketMessageInfo : WebSocketMessage<string>
+ public class WebSocketMessageInfo : InboundWebSocketMessage<string>
{
/// <summary>
/// Gets or sets the connection.
diff --git a/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs b/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs
new file mode 100644
index 000000000..11e5a6bb2
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs
@@ -0,0 +1,32 @@
+#pragma warning disable SA1649 // File name must equal class name.
+
+namespace MediaBrowser.Controller.Net;
+
+/// <summary>
+/// Class WebSocketMessage.
+/// </summary>
+/// <typeparam name="T">The type of the data.</typeparam>
+public abstract class WebSocketMessage<T> : WebSocketMessage
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="WebSocketMessage{T}"/> class.
+ /// </summary>
+ protected WebSocketMessage()
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="WebSocketMessage{T}"/> class.
+ /// </summary>
+ /// <param name="data">The data to send.</param>
+ protected WebSocketMessage(T data)
+ {
+ Data = data;
+ }
+
+ /// <summary>
+ /// Gets or sets the data.
+ /// </summary>
+ // TODO make this set only.
+ public T? Data { get; set; }
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/IInboundWebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/IInboundWebSocketMessage.cs
new file mode 100644
index 000000000..c3cf9955a
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/IInboundWebSocketMessage.cs
@@ -0,0 +1,10 @@
+#pragma warning disable CA1040
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages;
+
+/// <summary>
+/// Interface representing that the websocket message is inbound.
+/// </summary>
+public interface IInboundWebSocketMessage
+{
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/IOutboundWebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/IOutboundWebSocketMessage.cs
new file mode 100644
index 000000000..c74a254a6
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/IOutboundWebSocketMessage.cs
@@ -0,0 +1,10 @@
+#pragma warning disable CA1040
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages;
+
+/// <summary>
+/// Interface representing that the websocket message is outbound.
+/// </summary>
+public interface IOutboundWebSocketMessage
+{
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs
new file mode 100644
index 000000000..b3a60199a
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStartMessage.cs
@@ -0,0 +1,25 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound;
+
+/// <summary>
+/// Activity log entry start message.
+/// Data is the timing data encoded as "$initialDelay,$interval" in ms.
+/// </summary>
+public class ActivityLogEntryStartMessage : InboundWebSocketMessage<string>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ActivityLogEntryStartMessage"/> class.
+ /// Data is the timing data encoded as "$initialDelay,$interval" in ms.
+ /// </summary>
+ /// <param name="data">The timing data encoded as "$initialDelay,$interval".</param>
+ public ActivityLogEntryStartMessage(string data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.ActivityLogEntryStart)]
+ public override SessionMessageType MessageType => SessionMessageType.ActivityLogEntryStart;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs
new file mode 100644
index 000000000..6f65cb2c7
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ActivityLogEntryStopMessage.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound;
+
+/// <summary>
+/// Activity log entry stop message.
+/// </summary>
+public class ActivityLogEntryStopMessage : InboundWebSocketMessage
+{
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.ActivityLogEntryStop)]
+ public override SessionMessageType MessageType => SessionMessageType.ActivityLogEntryStop;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/InboundKeepAliveMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/InboundKeepAliveMessage.cs
new file mode 100644
index 000000000..fec7cb4e4
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/InboundKeepAliveMessage.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound;
+
+/// <summary>
+/// Keep alive websocket messages.
+/// </summary>
+public class InboundKeepAliveMessage : InboundWebSocketMessage
+{
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.KeepAlive)]
+ public override SessionMessageType MessageType => SessionMessageType.KeepAlive;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs
new file mode 100644
index 000000000..bf98470bf
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStartMessage.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound;
+
+/// <summary>
+/// Scheduled tasks info start message.
+/// Data is the timing data encoded as "$initialDelay,$interval" in ms.
+/// </summary>
+public class ScheduledTasksInfoStartMessage : InboundWebSocketMessage<string>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ScheduledTasksInfoStartMessage"/> class.
+ /// </summary>
+ /// <param name="data">The timing data encoded as $initialDelay,$interval.</param>
+ public ScheduledTasksInfoStartMessage(string data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.ScheduledTasksInfoStart)]
+ public override SessionMessageType MessageType => SessionMessageType.ScheduledTasksInfoStart;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs
new file mode 100644
index 000000000..f36739c70
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/ScheduledTasksInfoStopMessage.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound;
+
+/// <summary>
+/// Scheduled tasks info stop message.
+/// </summary>
+public class ScheduledTasksInfoStopMessage : InboundWebSocketMessage
+{
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.ScheduledTasksInfoStop)]
+ public override SessionMessageType MessageType => SessionMessageType.ScheduledTasksInfoStop;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs
new file mode 100644
index 000000000..a40a0c79e
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStartMessage.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound;
+
+/// <summary>
+/// Sessions start message.
+/// Data is the timing data encoded as "$initialDelay,$interval" in ms.
+/// </summary>
+public class SessionsStartMessage : InboundWebSocketMessage<string>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SessionsStartMessage"/> class.
+ /// </summary>
+ /// <param name="data">The timing data encoded as $initialDelay,$interval.</param>
+ public SessionsStartMessage(string data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.SessionsStart)]
+ public override SessionMessageType MessageType => SessionMessageType.SessionsStart;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs
new file mode 100644
index 000000000..288d111c5
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Inbound/SessionsStopMessage.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Inbound;
+
+/// <summary>
+/// Sessions stop message.
+/// </summary>
+public class SessionsStopMessage : InboundWebSocketMessage
+{
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.SessionsStop)]
+ public override SessionMessageType MessageType => SessionMessageType.SessionsStop;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs
new file mode 100644
index 000000000..8d6e821df
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessage.cs
@@ -0,0 +1,8 @@
+namespace MediaBrowser.Controller.Net.WebSocketMessages;
+
+/// <summary>
+/// Inbound websocket message.
+/// </summary>
+public class InboundWebSocketMessage : WebSocketMessage, IInboundWebSocketMessage
+{
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessageOfT.cs b/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessageOfT.cs
new file mode 100644
index 000000000..4da5e7d31
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/InboundWebSocketMessageOfT.cs
@@ -0,0 +1,26 @@
+#pragma warning disable SA1649 // File name must equal class name.
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages;
+
+/// <summary>
+/// Inbound websocket message with data.
+/// </summary>
+/// <typeparam name="T">The data type.</typeparam>
+public class InboundWebSocketMessage<T> : WebSocketMessage<T>, IInboundWebSocketMessage
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="InboundWebSocketMessage{T}"/> class.
+ /// </summary>
+ public InboundWebSocketMessage()
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="InboundWebSocketMessage{T}"/> class.
+ /// </summary>
+ /// <param name="data">The data to send.</param>
+ protected InboundWebSocketMessage(T data)
+ {
+ Data = data;
+ }
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs
new file mode 100644
index 000000000..2a098615d
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ActivityLogEntryMessage.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Activity log created message.
+/// </summary>
+public class ActivityLogEntryMessage : OutboundWebSocketMessage<IReadOnlyList<ActivityLogEntry>>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ActivityLogEntryMessage"/> class.
+ /// </summary>
+ /// <param name="data">List of activity log entries.</param>
+ public ActivityLogEntryMessage(IReadOnlyList<ActivityLogEntry> data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.ActivityLogEntry)]
+ public override SessionMessageType MessageType => SessionMessageType.ActivityLogEntry;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs
new file mode 100644
index 000000000..ca55340a0
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ForceKeepAliveMessage.cs
@@ -0,0 +1,23 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Force keep alive websocket messages.
+/// </summary>
+public class ForceKeepAliveMessage : OutboundWebSocketMessage<int>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ForceKeepAliveMessage"/> class.
+ /// </summary>
+ /// <param name="data">The timeout in seconds.</param>
+ public ForceKeepAliveMessage(int data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.ForceKeepAlive)]
+ public override SessionMessageType MessageType => SessionMessageType.ForceKeepAlive;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs
new file mode 100644
index 000000000..5fbbb0624
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/GeneralCommandMessage.cs
@@ -0,0 +1,23 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// General command websocket message.
+/// </summary>
+public class GeneralCommandMessage : OutboundWebSocketMessage<GeneralCommand>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GeneralCommandMessage"/> class.
+ /// </summary>
+ /// <param name="data">The general command.</param>
+ public GeneralCommandMessage(GeneralCommand data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.GeneralCommand)]
+ public override SessionMessageType MessageType => SessionMessageType.GeneralCommand;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs
new file mode 100644
index 000000000..47417c405
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/LibraryChangedMessage.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Library changed message.
+/// </summary>
+public class LibraryChangedMessage : OutboundWebSocketMessage<LibraryUpdateInfo>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LibraryChangedMessage"/> class.
+ /// </summary>
+ /// <param name="data">The library update info.</param>
+ public LibraryChangedMessage(LibraryUpdateInfo data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.LibraryChanged)]
+ public override SessionMessageType MessageType => SessionMessageType.LibraryChanged;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/OutboundKeepAliveMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/OutboundKeepAliveMessage.cs
new file mode 100644
index 000000000..d907dcff9
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/OutboundKeepAliveMessage.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Keep alive websocket messages.
+/// </summary>
+public class OutboundKeepAliveMessage : OutboundWebSocketMessage
+{
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.KeepAlive)]
+ public override SessionMessageType MessageType => SessionMessageType.KeepAlive;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs
new file mode 100644
index 000000000..86ee2ff90
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlayMessage.cs
@@ -0,0 +1,23 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Play command websocket message.
+/// </summary>
+public class PlayMessage : OutboundWebSocketMessage<PlayRequest>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PlayMessage"/> class.
+ /// </summary>
+ /// <param name="data">The play request.</param>
+ public PlayMessage(PlayRequest data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.Play)]
+ public override SessionMessageType MessageType => SessionMessageType.Play;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs
new file mode 100644
index 000000000..cd6d28cb3
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PlaystateMessage.cs
@@ -0,0 +1,23 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Playstate message.
+/// </summary>
+public class PlaystateMessage : OutboundWebSocketMessage<PlaystateRequest>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PlaystateMessage"/> class.
+ /// </summary>
+ /// <param name="data">Playstate request data.</param>
+ public PlaystateMessage(PlaystateRequest data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.Playstate)]
+ public override SessionMessageType MessageType => SessionMessageType.Playstate;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs
new file mode 100644
index 000000000..17fd25938
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCancelledMessage.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.Updates;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Plugin installation cancelled message.
+/// </summary>
+public class PluginInstallationCancelledMessage : OutboundWebSocketMessage<InstallationInfo>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PluginInstallationCancelledMessage"/> class.
+ /// </summary>
+ /// <param name="data">Installation info.</param>
+ public PluginInstallationCancelledMessage(InstallationInfo data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.PackageInstallationCancelled)]
+ public override SessionMessageType MessageType => SessionMessageType.PackageInstallationCancelled;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs
new file mode 100644
index 000000000..3e60198ba
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationCompletedMessage.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.Updates;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Plugin installation completed message.
+/// </summary>
+public class PluginInstallationCompletedMessage : OutboundWebSocketMessage<InstallationInfo>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PluginInstallationCompletedMessage"/> class.
+ /// </summary>
+ /// <param name="data">Installation info.</param>
+ public PluginInstallationCompletedMessage(InstallationInfo data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.PackageInstallationCompleted)]
+ public override SessionMessageType MessageType => SessionMessageType.PackageInstallationCompleted;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs
new file mode 100644
index 000000000..40032f16e
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallationFailedMessage.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.Updates;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Plugin installation failed message.
+/// </summary>
+public class PluginInstallationFailedMessage : OutboundWebSocketMessage<InstallationInfo>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PluginInstallationFailedMessage"/> class.
+ /// </summary>
+ /// <param name="data">Installation info.</param>
+ public PluginInstallationFailedMessage(InstallationInfo data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.PackageInstallationFailed)]
+ public override SessionMessageType MessageType => SessionMessageType.PackageInstallationFailed;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs
new file mode 100644
index 000000000..28861896f
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginInstallingMessage.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.Updates;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Package installing message.
+/// </summary>
+public class PluginInstallingMessage : OutboundWebSocketMessage<InstallationInfo>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PluginInstallingMessage"/> class.
+ /// </summary>
+ /// <param name="data">Installation info.</param>
+ public PluginInstallingMessage(InstallationInfo data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.PackageInstalling)]
+ public override SessionMessageType MessageType => SessionMessageType.PackageInstalling;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs
new file mode 100644
index 000000000..ca4959119
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/PluginUninstalledMessage.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Plugin uninstalled message.
+/// </summary>
+public class PluginUninstalledMessage : OutboundWebSocketMessage<PluginInfo>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PluginUninstalledMessage"/> class.
+ /// </summary>
+ /// <param name="data">Plugin info.</param>
+ public PluginUninstalledMessage(PluginInfo data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.PackageUninstalled)]
+ public override SessionMessageType MessageType => SessionMessageType.PackageUninstalled;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs
new file mode 100644
index 000000000..41b3cd46a
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RefreshProgressMessage.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Refresh progress message.
+/// </summary>
+public class RefreshProgressMessage : OutboundWebSocketMessage<Dictionary<string, string>>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RefreshProgressMessage"/> class.
+ /// </summary>
+ /// <param name="data">Refresh progress data.</param>
+ public RefreshProgressMessage(Dictionary<string, string> data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.RefreshProgress)]
+ public override SessionMessageType MessageType => SessionMessageType.RefreshProgress;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs
new file mode 100644
index 000000000..a89f19b61
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/RestartRequiredMessage.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Restart required.
+/// </summary>
+public class RestartRequiredMessage : OutboundWebSocketMessage
+{
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.RestartRequired)]
+ public override SessionMessageType MessageType => SessionMessageType.RestartRequired;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs
new file mode 100644
index 000000000..afa36fb72
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTaskEndedMessage.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.Tasks;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Scheduled task ended message.
+/// </summary>
+public class ScheduledTaskEndedMessage : OutboundWebSocketMessage<TaskResult>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ScheduledTaskEndedMessage"/> class.
+ /// </summary>
+ /// <param name="data">Task result.</param>
+ public ScheduledTaskEndedMessage(TaskResult data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.ScheduledTaskEnded)]
+ public override SessionMessageType MessageType => SessionMessageType.ScheduledTaskEnded;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs
new file mode 100644
index 000000000..c7360779f
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ScheduledTasksInfoMessage.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.Tasks;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Scheduled tasks info message.
+/// </summary>
+public class ScheduledTasksInfoMessage : OutboundWebSocketMessage<IReadOnlyList<TaskInfo>>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ScheduledTasksInfoMessage"/> class.
+ /// </summary>
+ /// <param name="data">List of task infos.</param>
+ public ScheduledTasksInfoMessage(IReadOnlyList<TaskInfo> data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.ScheduledTasksInfo)]
+ public override SessionMessageType MessageType => SessionMessageType.ScheduledTasksInfo;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs
new file mode 100644
index 000000000..f832c8935
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCancelledMessage.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Series timer cancelled message.
+/// </summary>
+public class SeriesTimerCancelledMessage : OutboundWebSocketMessage<TimerEventInfo>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SeriesTimerCancelledMessage"/> class.
+ /// </summary>
+ /// <param name="data">The timer event info.</param>
+ public SeriesTimerCancelledMessage(TimerEventInfo data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.SeriesTimerCancelled)]
+ public override SessionMessageType MessageType => SessionMessageType.SeriesTimerCancelled;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs
new file mode 100644
index 000000000..450b4c799
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SeriesTimerCreatedMessage.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Series timer created message.
+/// </summary>
+public class SeriesTimerCreatedMessage : OutboundWebSocketMessage<TimerEventInfo>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SeriesTimerCreatedMessage"/> class.
+ /// </summary>
+ /// <param name="data">timer event info.</param>
+ public SeriesTimerCreatedMessage(TimerEventInfo data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.SeriesTimerCreated)]
+ public override SessionMessageType MessageType => SessionMessageType.SeriesTimerCreated;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs
new file mode 100644
index 000000000..8f09c802f
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerRestartingMessage.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Server restarting down message.
+/// </summary>
+public class ServerRestartingMessage : OutboundWebSocketMessage
+{
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.ServerRestarting)]
+ public override SessionMessageType MessageType => SessionMessageType.ServerRestarting;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs
new file mode 100644
index 000000000..485e71b6e
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/ServerShuttingDownMessage.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Server shutting down message.
+/// </summary>
+public class ServerShuttingDownMessage : OutboundWebSocketMessage
+{
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.ServerShuttingDown)]
+ public override SessionMessageType MessageType => SessionMessageType.ServerShuttingDown;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs
new file mode 100644
index 000000000..3504831b8
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Sessions message.
+/// </summary>
+public class SessionsMessage : OutboundWebSocketMessage<IReadOnlyList<SessionInfo>>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SessionsMessage"/> class.
+ /// </summary>
+ /// <param name="data">Session info.</param>
+ public SessionsMessage(IReadOnlyList<SessionInfo> data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.Sessions)]
+ public override SessionMessageType MessageType => SessionMessageType.Sessions;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs
new file mode 100644
index 000000000..d0624ec01
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayCommandMessage.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Sync play command.
+/// </summary>
+public class SyncPlayCommandMessage : OutboundWebSocketMessage<SendCommand>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SyncPlayCommandMessage"/> class.
+ /// </summary>
+ /// <param name="data">The send command.</param>
+ public SyncPlayCommandMessage(SendCommand data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.SyncPlayCommand)]
+ public override SessionMessageType MessageType => SessionMessageType.SyncPlayCommand;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs
new file mode 100644
index 000000000..6a501aa7e
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Untyped sync play command.
+/// </summary>
+public class SyncPlayGroupUpdateCommandMessage : OutboundWebSocketMessage<GroupUpdate>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandMessage"/> class.
+ /// </summary>
+ /// <param name="data">The send command.</param>
+ public SyncPlayGroupUpdateCommandMessage(GroupUpdate data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)]
+ public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs
new file mode 100644
index 000000000..47f706e2a
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs
@@ -0,0 +1,25 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Sync play group update command with group info.
+/// GroupUpdateTypes: GroupJoined.
+/// </summary>
+public class SyncPlayGroupUpdateCommandOfGroupInfoMessage : OutboundWebSocketMessage<GroupUpdate<GroupInfoDto>>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupInfoMessage"/> class.
+ /// </summary>
+ /// <param name="data">The group info.</param>
+ public SyncPlayGroupUpdateCommandOfGroupInfoMessage(GroupUpdate<GroupInfoDto> data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)]
+ public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs
new file mode 100644
index 000000000..11ddb1e25
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs
@@ -0,0 +1,25 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Sync play group update command with group state update.
+/// GroupUpdateTypes: StateUpdate.
+/// </summary>
+public class SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage : OutboundWebSocketMessage<GroupUpdate<GroupStateUpdate>>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage"/> class.
+ /// </summary>
+ /// <param name="data">The group info.</param>
+ public SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage(GroupUpdate<GroupStateUpdate> data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)]
+ public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs
new file mode 100644
index 000000000..7e73399b1
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs
@@ -0,0 +1,25 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Sync play group update command with play queue update.
+/// GroupUpdateTypes: PlayQueue.
+/// </summary>
+public class SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage : OutboundWebSocketMessage<GroupUpdate<PlayQueueUpdate>>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage"/> class.
+ /// </summary>
+ /// <param name="data">The play queue update.</param>
+ public SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage(GroupUpdate<PlayQueueUpdate> data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)]
+ public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs
new file mode 100644
index 000000000..5b5ccd3ed
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs
@@ -0,0 +1,25 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Sync play group update command with string.
+/// GroupUpdateTypes: GroupDoesNotExist (error), LibraryAccessDenied (error), NotInGroup (error), GroupLeft (groupId), UserJoined (username), UserLeft (username).
+/// </summary>
+public class SyncPlayGroupUpdateCommandOfStringMessage : OutboundWebSocketMessage<GroupUpdate<string>>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SyncPlayGroupUpdateCommandOfStringMessage"/> class.
+ /// </summary>
+ /// <param name="data">The send command.</param>
+ public SyncPlayGroupUpdateCommandOfStringMessage(GroupUpdate<string> data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)]
+ public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs
new file mode 100644
index 000000000..f44fd126b
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCancelledMessage.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Timer cancelled message.
+/// </summary>
+public class TimerCancelledMessage : OutboundWebSocketMessage<TimerEventInfo>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TimerCancelledMessage"/> class.
+ /// </summary>
+ /// <param name="data">Timer event info.</param>
+ public TimerCancelledMessage(TimerEventInfo data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.TimerCancelled)]
+ public override SessionMessageType MessageType => SessionMessageType.TimerCancelled;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs
new file mode 100644
index 000000000..8c1e102eb
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/TimerCreatedMessage.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// Timer created message.
+/// </summary>
+public class TimerCreatedMessage : OutboundWebSocketMessage<TimerEventInfo>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TimerCreatedMessage"/> class.
+ /// </summary>
+ /// <param name="data">Timer event info.</param>
+ public TimerCreatedMessage(TimerEventInfo data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.TimerCreated)]
+ public override SessionMessageType MessageType => SessionMessageType.TimerCreated;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs
new file mode 100644
index 000000000..6a053643d
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDataChangedMessage.cs
@@ -0,0 +1,23 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// User data changed message.
+/// </summary>
+public class UserDataChangedMessage : OutboundWebSocketMessage<UserDataChangeInfo>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserDataChangedMessage"/> class.
+ /// </summary>
+ /// <param name="data">The data change info.</param>
+ public UserDataChangedMessage(UserDataChangeInfo data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.UserDataChanged)]
+ public override SessionMessageType MessageType => SessionMessageType.UserDataChanged;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs
new file mode 100644
index 000000000..add3f7771
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserDeletedMessage.cs
@@ -0,0 +1,24 @@
+using System;
+using System.ComponentModel;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// User deleted message.
+/// </summary>
+public class UserDeletedMessage : OutboundWebSocketMessage<Guid>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserDeletedMessage"/> class.
+ /// </summary>
+ /// <param name="data">The user id.</param>
+ public UserDeletedMessage(Guid data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.UserDeleted)]
+ public override SessionMessageType MessageType => SessionMessageType.UserDeleted;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs
new file mode 100644
index 000000000..9a72deae1
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/UserUpdatedMessage.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Session;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
+
+/// <summary>
+/// User updated message.
+/// </summary>
+public class UserUpdatedMessage : OutboundWebSocketMessage<UserDto>
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="UserUpdatedMessage"/> class.
+ /// </summary>
+ /// <param name="data">The user dto.</param>
+ public UserUpdatedMessage(UserDto data)
+ : base(data)
+ {
+ }
+
+ /// <inheritdoc />
+ [DefaultValue(SessionMessageType.UserUpdated)]
+ public override SessionMessageType MessageType => SessionMessageType.UserUpdated;
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs
new file mode 100644
index 000000000..178245851
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessage.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages;
+
+/// <summary>
+/// Outbound websocket message.
+/// </summary>
+public class OutboundWebSocketMessage : WebSocketMessage, IOutboundWebSocketMessage
+{
+ /// <summary>
+ /// Gets or sets the message id.
+ /// </summary>
+ public Guid MessageId { get; set; } = Guid.NewGuid();
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessageOfT.cs b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessageOfT.cs
new file mode 100644
index 000000000..cce331805
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessageOfT.cs
@@ -0,0 +1,33 @@
+#pragma warning disable SA1649 // File name must equal class name.
+
+using System;
+
+namespace MediaBrowser.Controller.Net.WebSocketMessages;
+
+/// <summary>
+/// Outbound websocket message with data.
+/// </summary>
+/// <typeparam name="T">The data type.</typeparam>
+public class OutboundWebSocketMessage<T> : WebSocketMessage<T>, IOutboundWebSocketMessage
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OutboundWebSocketMessage{T}"/> class.
+ /// </summary>
+ public OutboundWebSocketMessage()
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OutboundWebSocketMessage{T}"/> class.
+ /// </summary>
+ /// <param name="data">The data to send.</param>
+ protected OutboundWebSocketMessage(T data)
+ {
+ Data = data;
+ }
+
+ /// <summary>
+ /// Gets or sets the message id.
+ /// </summary>
+ public Guid MessageId { get; set; } = Guid.NewGuid();
+}
diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
index d88943662..bb68a3b6d 100644
--- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
+++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
@@ -44,6 +44,12 @@ namespace MediaBrowser.Controller.Playlists
/// <summary>
/// Gets the playlists folder.
/// </summary>
+ /// <returns>Folder.</returns>
+ Folder GetPlaylistsFolder();
+
+ /// <summary>
+ /// Gets the playlists folder for a user.
+ /// </summary>
/// <param name="userId">The user identifier.</param>
/// <returns>Folder.</returns>
Folder GetPlaylistsFolder(Guid userId);
@@ -66,10 +72,9 @@ namespace MediaBrowser.Controller.Playlists
Task RemovePlaylistsAsync(Guid userId);
/// <summary>
- /// Updates a playlist.
+ /// Saves a playlist.
/// </summary>
- /// <param name="playlist">The updated playlist.</param>
- /// <returns>Task.</returns>
- Task UpdatePlaylistAsync(Playlist playlist);
+ /// <param name="item">The playlist.</param>
+ void SavePlaylistFile(Playlist item);
}
}
diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs
index 344e996ea..498df5ab0 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -34,10 +34,13 @@ namespace MediaBrowser.Controller.Playlists
public Playlist()
{
Shares = Array.Empty<Share>();
+ OpenAccess = false;
}
public Guid OwnerUserId { get; set; }
+ public bool OpenAccess { get; set; }
+
public Share[] Shares { get; set; }
[JsonIgnore]
@@ -233,6 +236,11 @@ namespace MediaBrowser.Controller.Playlists
return base.IsVisible(user);
}
+ if (OpenAccess)
+ {
+ return true;
+ }
+
var userId = user.Id;
if (userId.Equals(OwnerUserId))
{
diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs
index 7e0a69586..16943f6aa 100644
--- a/MediaBrowser.Controller/Providers/IProviderManager.cs
+++ b/MediaBrowser.Controller/Providers/IProviderManager.cs
@@ -55,14 +55,6 @@ namespace MediaBrowser.Controller.Providers
Task<ItemUpdateType> RefreshSingleItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken);
/// <summary>
- /// Runs multiple metadata refreshes concurrently.
- /// </summary>
- /// <param name="action">The action to run.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
- Task RunMetadataRefresh(Func<Task> action, CancellationToken cancellationToken);
-
- /// <summary>
/// Saves the image.
/// </summary>
/// <param name="item">The item.</param>
@@ -207,15 +199,6 @@ namespace MediaBrowser.Controller.Providers
where TItemType : BaseItem, new()
where TLookupType : ItemLookupInfo;
- /// <summary>
- /// Gets the search image.
- /// </summary>
- /// <param name="providerName">Name of the provider.</param>
- /// <param name="url">The URL.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{HttpResponseInfo}.</returns>
- Task<HttpResponseMessage> GetSearchImage(string providerName, string url, CancellationToken cancellationToken);
-
HashSet<Guid> GetRefreshQueue();
void OnRefreshStart(BaseItem item);
diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
index b95d00aa3..282aa721e 100644
--- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
@@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>BaseItem.</returns>
- BaseItem ResolvePath(ItemResolveArgs args);
+ BaseItem? ResolvePath(ItemResolveArgs args);
}
public interface IMultiItemResolver
diff --git a/MediaBrowser.Controller/Resolvers/ItemResolver.cs b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
index a6da8384e..5c9dd6f07 100644
--- a/MediaBrowser.Controller/Resolvers/ItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -23,7 +21,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>`0.</returns>
- protected internal virtual T Resolve(ItemResolveArgs args)
+ protected internal virtual T? Resolve(ItemResolveArgs args)
{
return null;
}
@@ -42,7 +40,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>BaseItem.</returns>
- public BaseItem ResolvePath(ItemResolveArgs args)
+ public BaseItem? ResolvePath(ItemResolveArgs args)
{
var item = Resolve(args);
diff --git a/MediaBrowser.Controller/Security/IAuthenticationManager.cs b/MediaBrowser.Controller/Security/IAuthenticationManager.cs
index e3d18c8c0..070ab7a85 100644
--- a/MediaBrowser.Controller/Security/IAuthenticationManager.cs
+++ b/MediaBrowser.Controller/Security/IAuthenticationManager.cs
@@ -1,6 +1,4 @@
-#nullable enable
-
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Threading.Tasks;
namespace MediaBrowser.Controller.Security
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 0c4719a0e..53df7133b 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -233,20 +233,6 @@ namespace MediaBrowser.Controller.Session
Task SendRestartRequiredNotification(CancellationToken cancellationToken);
/// <summary>
- /// Sends the server shutdown notification.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task SendServerShutdownNotification(CancellationToken cancellationToken);
-
- /// <summary>
- /// Sends the server restart notification.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task SendServerRestartNotification(CancellationToken cancellationToken);
-
- /// <summary>
/// Adds the additional user.
/// </summary>
/// <param name="sessionId">The session identifier.</param>
diff --git a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs
index 85b3e6fbd..51c29c7a2 100644
--- a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs
+++ b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs
@@ -14,6 +14,8 @@ namespace MediaBrowser.Controller.Subtitles
public bool IsForced { get; set; }
+ public bool IsHearingImpaired { get; set; }
+
public Stream Stream { get; set; }
}
}
diff --git a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
index bdebbbfd4..c0a168192 100644
--- a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
+++ b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
@@ -23,13 +23,13 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// The sorted playlist.
/// </summary>
/// <value>The sorted playlist, or play queue of the group.</value>
- private List<QueueItem> _sortedPlaylist = new List<QueueItem>();
+ private List<SyncPlayQueueItem> _sortedPlaylist = new List<SyncPlayQueueItem>();
/// <summary>
/// The shuffled playlist.
/// </summary>
/// <value>The shuffled playlist, or play queue of the group.</value>
- private List<QueueItem> _shuffledPlaylist = new List<QueueItem>();
+ private List<SyncPlayQueueItem> _shuffledPlaylist = new List<SyncPlayQueueItem>();
/// <summary>
/// Initializes a new instance of the <see cref="PlayQueueManager" /> class.
@@ -76,7 +76,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// Gets the current playlist considering the shuffle mode.
/// </summary>
/// <returns>The playlist.</returns>
- public IReadOnlyList<QueueItem> GetPlaylist()
+ public IReadOnlyList<SyncPlayQueueItem> GetPlaylist()
{
return GetPlaylistInternal();
}
@@ -93,7 +93,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
_sortedPlaylist = CreateQueueItemsFromArray(items);
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{
- _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
+ _shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist);
_shuffledPlaylist.Shuffle();
}
@@ -125,14 +125,14 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
if (PlayingItemIndex == NoPlayingItemIndex)
{
- _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
+ _shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist);
_shuffledPlaylist.Shuffle();
}
else if (ShuffleMode.Equals(GroupShuffleMode.Sorted))
{
// First time shuffle.
var playingItem = _sortedPlaylist[PlayingItemIndex];
- _shuffledPlaylist = new List<QueueItem>(_sortedPlaylist);
+ _shuffledPlaylist = new List<SyncPlayQueueItem>(_sortedPlaylist);
_shuffledPlaylist.RemoveAt(PlayingItemIndex);
_shuffledPlaylist.Shuffle();
_shuffledPlaylist.Insert(0, playingItem);
@@ -407,7 +407,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// Gets the next item in the playlist considering repeat mode and shuffle mode.
/// </summary>
/// <returns>The next item in the playlist.</returns>
- public QueueItem GetNextItemPlaylistId()
+ public SyncPlayQueueItem GetNextItemPlaylistId()
{
int newIndex;
var playlist = GetPlaylistInternal();
@@ -502,12 +502,12 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// Creates a list from the array of items. Each item is given an unique playlist identifier.
/// </summary>
/// <returns>The list of queue items.</returns>
- private List<QueueItem> CreateQueueItemsFromArray(IReadOnlyList<Guid> items)
+ private List<SyncPlayQueueItem> CreateQueueItemsFromArray(IReadOnlyList<Guid> items)
{
- var list = new List<QueueItem>();
+ var list = new List<SyncPlayQueueItem>();
foreach (var item in items)
{
- var queueItem = new QueueItem(item);
+ var queueItem = new SyncPlayQueueItem(item);
list.Add(queueItem);
}
@@ -518,7 +518,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// Gets the current playlist considering the shuffle mode.
/// </summary>
/// <returns>The playlist.</returns>
- private List<QueueItem> GetPlaylistInternal()
+ private List<SyncPlayQueueItem> GetPlaylistInternal()
{
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{
@@ -532,7 +532,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
/// Gets the current playing item, depending on the shuffle mode.
/// </summary>
/// <returns>The playing item.</returns>
- private QueueItem GetPlayingItem()
+ private SyncPlayQueueItem GetPlayingItem()
{
if (PlayingItemIndex == NoPlayingItemIndex)
{