aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Controller')
-rw-r--r--MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs65
-rw-r--r--MediaBrowser.Controller/BaseItemManager/IBaseItemManager.cs5
-rw-r--r--MediaBrowser.Controller/Channels/IChannelManager.cs4
-rw-r--r--MediaBrowser.Controller/Collections/ICollectionManager.cs7
-rw-r--r--MediaBrowser.Controller/Entities/AggregateFolder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Audio/MusicArtist.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs86
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs42
-rw-r--r--MediaBrowser.Controller/Entities/IHasShares.cs11
-rw-r--r--MediaBrowser.Controller/Entities/PeopleHelper.cs23
-rw-r--r--MediaBrowser.Controller/Entities/PersonInfo.cs9
-rw-r--r--MediaBrowser.Controller/Entities/Share.cs13
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs4
-rw-r--r--MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs13
-rw-r--r--MediaBrowser.Controller/Library/IUserManager.cs16
-rw-r--r--MediaBrowser.Controller/Library/ItemResolveArgs.cs25
-rw-r--r--MediaBrowser.Controller/Library/MetadataConfigurationExtensions.cs17
-rw-r--r--MediaBrowser.Controller/LiveTv/ILiveTvManager.cs4
-rw-r--r--MediaBrowser.Controller/LiveTv/LiveTvProgram.cs6
-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/Lyrics/LyricMetadata.cs2
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs1074
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs10
-rw-r--r--MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs30
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs4
-rw-r--r--MediaBrowser.Controller/Net/IWebSocketConnection.cs1
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessage.cs22
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessageInfo.cs2
-rw-r--r--MediaBrowser.Controller/Net/WebSocketMessageOfT.cs33
-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.cs14
-rw-r--r--MediaBrowser.Controller/Playlists/Playlist.cs15
-rw-r--r--MediaBrowser.Controller/Providers/IProviderManager.cs17
-rw-r--r--MediaBrowser.Controller/Providers/ImageRefreshOptions.cs3
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs1
-rw-r--r--MediaBrowser.Controller/Subtitles/ISubtitleManager.cs1
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs8
-rw-r--r--MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs53
86 files changed, 1999 insertions, 748 deletions
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/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs
index e392a3493..8eb27888a 100644
--- a/MediaBrowser.Controller/Channels/IChannelManager.cs
+++ b/MediaBrowser.Controller/Channels/IChannelManager.cs
@@ -46,14 +46,14 @@ namespace MediaBrowser.Controller.Channels
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The channels.</returns>
- QueryResult<Channel> GetChannelsInternal(ChannelQuery query);
+ Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query);
/// <summary>
/// Gets the channels.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The channels.</returns>
- QueryResult<BaseItemDto> GetChannels(ChannelQuery query);
+ Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query);
/// <summary>
/// Gets the latest channel items.
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/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs
index 08c622cde..d789033f1 100644
--- a/MediaBrowser.Controller/Entities/AggregateFolder.cs
+++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs
@@ -120,7 +120,7 @@ namespace MediaBrowser.Controller.Entities
var path = ContainingFolderPath;
- var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
+ var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager)
{
FileInfo = FileSystem.GetDirectoryInfo(path)
};
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 b8601cccd..501811003 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -47,7 +47,7 @@ namespace MediaBrowser.Controller.Entities
/// The supported image extensions.
/// </summary>
public static readonly string[] SupportedImageExtensions
- = new[] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" };
+ = new[] { ".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif" };
private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions)
{
@@ -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>
@@ -801,16 +808,14 @@ namespace MediaBrowser.Controller.Entities
{
return allowed.Contains(ChannelId);
}
- else
- {
- var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
- foreach (var folder in collectionFolders)
+ var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
+
+ foreach (var folder in collectionFolders)
+ {
+ if (allowed.Contains(folder.Id))
{
- if (allowed.Contains(folder.Id))
- {
- return true;
- }
+ return true;
}
}
@@ -893,16 +898,6 @@ namespace MediaBrowser.Controller.Entities
var sortable = Name.Trim().ToLowerInvariant();
- foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
- {
- sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
- }
-
- foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
- {
- sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
- }
-
foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
{
// Remove from beginning if a space follows
@@ -921,12 +916,22 @@ namespace MediaBrowser.Controller.Entities
}
}
+ foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
+ {
+ sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
+ }
+
+ foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
+ {
+ sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
+ }
+
return ModifySortChunks(sortable);
}
- internal static string ModifySortChunks(string name)
+ internal static string ModifySortChunks(ReadOnlySpan<char> name)
{
- void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
+ static void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
{
if (isDigitChunk && chunk.Length < 10)
{
@@ -936,7 +941,7 @@ namespace MediaBrowser.Controller.Entities
builder.Append(chunk);
}
- if (name.Length == 0)
+ if (name.IsEmpty)
{
return string.Empty;
}
@@ -950,13 +955,13 @@ namespace MediaBrowser.Controller.Entities
var isDigit = char.IsDigit(name[i]);
if (isDigit != isDigitChunk)
{
- AppendChunk(builder, isDigitChunk, name.AsSpan(chunkStart, i - chunkStart));
+ AppendChunk(builder, isDigitChunk, name.Slice(chunkStart, i - chunkStart));
chunkStart = i;
isDigitChunk = isDigit;
}
}
- AppendChunk(builder, isDigitChunk, name.AsSpan(chunkStart));
+ AppendChunk(builder, isDigitChunk, name.Slice(chunkStart));
// logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
return builder.ToString().RemoveDiacritics();
@@ -1239,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>
@@ -1255,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)
@@ -1276,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)
@@ -1362,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)
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index 5ac619d8f..095b261c0 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -288,7 +288,7 @@ namespace MediaBrowser.Controller.Entities
{
var path = ContainingFolderPath;
- var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
+ var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager)
{
FileInfo = FileSystem.GetDirectoryInfo(path),
Parent = GetParent() as Folder,
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/IHasShares.cs b/MediaBrowser.Controller/Entities/IHasShares.cs
deleted file mode 100644
index e6fa27703..000000000
--- a/MediaBrowser.Controller/Entities/IHasShares.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#nullable disable
-
-#pragma warning disable CA1819, CS1591
-
-namespace MediaBrowser.Controller.Entities
-{
- public interface IHasShares
- {
- Share[] Shares { get; set; }
- }
-}
diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs
index 7f8dc069c..5292bd772 100644
--- a/MediaBrowser.Controller/Entities/PeopleHelper.cs
+++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Entities
@@ -17,38 +18,38 @@ namespace MediaBrowser.Controller.Entities
// Normalize
if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
{
- person.Type = PersonType.GuestStar;
+ person.Type = PersonKind.GuestStar;
}
else if (string.Equals(person.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase))
{
- person.Type = PersonType.Director;
+ person.Type = PersonKind.Director;
}
else if (string.Equals(person.Role, PersonType.Producer, StringComparison.OrdinalIgnoreCase))
{
- person.Type = PersonType.Producer;
+ person.Type = PersonKind.Producer;
}
else if (string.Equals(person.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase))
{
- person.Type = PersonType.Writer;
+ person.Type = PersonKind.Writer;
}
// If the type is GuestStar and there's already an Actor entry, then update it to avoid dupes
- if (string.Equals(person.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
+ if (person.Type == PersonKind.GuestStar)
{
- var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase));
+ var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type == PersonKind.Actor);
if (existing is not null)
{
- existing.Type = PersonType.GuestStar;
+ existing.Type = PersonKind.GuestStar;
MergeExisting(existing, person);
return;
}
}
- if (string.Equals(person.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase))
+ if (person.Type == PersonKind.Actor)
{
// If the actor already exists without a role and we have one, fill it in
- var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && (p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase) || p.Type.Equals(PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)));
+ var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && (p.Type == PersonKind.Actor || p.Type == PersonKind.GuestStar));
if (existing is null)
{
// Wasn't there - add it
@@ -68,8 +69,8 @@ namespace MediaBrowser.Controller.Entities
else
{
var existing = people.FirstOrDefault(p =>
- string.Equals(p.Name, person.Name, StringComparison.OrdinalIgnoreCase) &&
- string.Equals(p.Type, person.Type, StringComparison.OrdinalIgnoreCase));
+ string.Equals(p.Name, person.Name, StringComparison.OrdinalIgnoreCase)
+ && p.Type == person.Type);
// Check for dupes based on the combination of Name and Type
if (existing is null)
diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs
index 2b689ae7e..3df0b0b78 100644
--- a/MediaBrowser.Controller/Entities/PersonInfo.cs
+++ b/MediaBrowser.Controller/Entities/PersonInfo.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Entities
@@ -36,7 +37,7 @@ namespace MediaBrowser.Controller.Entities
/// Gets or sets the type.
/// </summary>
/// <value>The type.</value>
- public string Type { get; set; }
+ public PersonKind Type { get; set; }
/// <summary>
/// Gets or sets the ascending sort order.
@@ -57,10 +58,6 @@ namespace MediaBrowser.Controller.Entities
return Name;
}
- public bool IsType(string type)
- {
- return string.Equals(Type, type, StringComparison.OrdinalIgnoreCase)
- || string.Equals(Role, type, StringComparison.OrdinalIgnoreCase);
- }
+ public bool IsType(PersonKind type) => Type == type || string.Equals(type.ToString(), Role, StringComparison.OrdinalIgnoreCase);
}
}
diff --git a/MediaBrowser.Controller/Entities/Share.cs b/MediaBrowser.Controller/Entities/Share.cs
deleted file mode 100644
index 64f446eef..000000000
--- a/MediaBrowser.Controller/Entities/Share.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Controller.Entities
-{
- public class Share
- {
- public string UserId { get; set; }
-
- public bool CanEdit { 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/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/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 01986d303..c70102167 100644
--- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs
+++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs
@@ -1,12 +1,11 @@
#nullable disable
-#pragma warning disable CA1721, CA1819, CS1591
+#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
@@ -23,22 +22,20 @@ namespace MediaBrowser.Controller.Library
/// </summary>
private readonly IServerApplicationPaths _appPaths;
+ private readonly ILibraryManager _libraryManager;
private LibraryOptions _libraryOptions;
/// <summary>
/// Initializes a new instance of the <see cref="ItemResolveArgs" /> class.
/// </summary>
/// <param name="appPaths">The app paths.</param>
- /// <param name="directoryService">The directory service.</param>
- public ItemResolveArgs(IServerApplicationPaths appPaths, IDirectoryService directoryService)
+ /// <param name="libraryManager">The library manager.</param>
+ public ItemResolveArgs(IServerApplicationPaths appPaths, ILibraryManager libraryManager)
{
_appPaths = appPaths;
- DirectoryService = directoryService;
+ _libraryManager = libraryManager;
}
- // TODO remove dependencies as properties, they should be injected where it makes sense
- public IDirectoryService DirectoryService { get; }
-
/// <summary>
/// Gets or sets the file system children.
/// </summary>
@@ -47,7 +44,7 @@ namespace MediaBrowser.Controller.Library
public LibraryOptions LibraryOptions
{
- get => _libraryOptions ??= Parent is null ? new LibraryOptions() : BaseItem.LibraryManager.GetLibraryOptions(Parent);
+ get => _libraryOptions ??= Parent is null ? new LibraryOptions() : _libraryManager.GetLibraryOptions(Parent);
set => _libraryOptions = value;
}
@@ -231,21 +228,15 @@ namespace MediaBrowser.Controller.Library
/// <summary>
/// Gets the configured content type for the path.
/// </summary>
- /// <remarks>
- /// This is subject to future refactoring as it relies on a static property in BaseItem.
- /// </remarks>
/// <returns>The configured content type.</returns>
public string GetConfiguredContentType()
{
- return BaseItem.LibraryManager.GetConfiguredContentType(Path);
+ return _libraryManager.GetConfiguredContentType(Path);
}
/// <summary>
/// Gets the file system children that do not hit the ignore file check.
/// </summary>
- /// <remarks>
- /// This is subject to future refactoring as it relies on a static property in BaseItem.
- /// </remarks>
/// <returns>The file system children that are not ignored.</returns>
public IEnumerable<FileSystemMetadata> GetActualFileSystemChildren()
{
@@ -253,7 +244,7 @@ namespace MediaBrowser.Controller.Library
for (var i = 0; i < numberOfChildren; i++)
{
var child = FileSystemChildren[i];
- if (BaseItem.LibraryManager.IgnoreFile(child, Parent))
+ if (_libraryManager.IgnoreFile(child, Parent))
{
continue;
}
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/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
index 46bdca302..3b6a16dee 100644
--- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
+++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
@@ -97,7 +97,7 @@ namespace MediaBrowser.Controller.LiveTv
/// <param name="query">The query.</param>
/// <param name="options">The options.</param>
/// <returns>A recording.</returns>
- QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options);
+ Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options);
/// <summary>
/// Gets the timers.
@@ -308,6 +308,6 @@ namespace MediaBrowser.Controller.LiveTv
void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, User user = null);
- List<BaseItem> GetRecordingFolders(User user);
+ Task<BaseItem[]> GetRecordingFoldersAsync(User user);
}
}
diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
index 514323238..c721fb778 100644
--- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
+++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs
@@ -197,10 +197,8 @@ namespace MediaBrowser.Controller.LiveTv
{
return 2.0 / 3;
}
- else
- {
- return 16.0 / 9;
- }
+
+ return 16.0 / 9;
}
public override string GetClientTypeName()
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/Lyrics/LyricMetadata.cs b/MediaBrowser.Controller/Lyrics/LyricMetadata.cs
index 6091ede52..c4f033489 100644
--- a/MediaBrowser.Controller/Lyrics/LyricMetadata.cs
+++ b/MediaBrowser.Controller/Lyrics/LyricMetadata.cs
@@ -1,5 +1,3 @@
-using System;
-
namespace MediaBrowser.Controller.Lyrics;
/// <summary>
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index f2fb3705c..c817cdfd9 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -43,6 +43,11 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _maxKerneli915Hang = new Version(6, 1, 3);
private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18);
+ 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 static readonly string[] _videoProfilesH264 = new[]
{
"ConstrainedBaseline",
@@ -61,6 +66,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",
@@ -86,6 +98,16 @@ namespace MediaBrowser.Controller.MediaEncoding
{ "truehd", 6 },
};
+ public static readonly string[] LosslessAudioCodecs = new string[]
+ {
+ "alac",
+ "ape",
+ "flac",
+ "mlp",
+ "truehd",
+ "wavpack"
+ };
+
public EncodingHelper(
IApplicationPaths appPaths,
IMediaEncoder mediaEncoder,
@@ -99,12 +121,15 @@ namespace MediaBrowser.Controller.MediaEncoding
}
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
@@ -125,14 +150,10 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(hwType)
&& encodingOptions.EnableHardwareEncoding
- && codecMap.ContainsKey(hwType))
+ && codecMap.TryGetValue(hwType, out var preferredEncoder)
+ && _mediaEncoder.SupportsEncoder(preferredEncoder))
{
- var preferredEncoder = codecMap[hwType];
-
- if (_mediaEncoder.SupportsEncoder(preferredEncoder))
- {
- return preferredEncoder;
- }
+ return preferredEncoder;
}
}
@@ -153,7 +174,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")
@@ -198,8 +220,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;
@@ -210,9 +232,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)
@@ -224,7 +246,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;
}
@@ -239,8 +261,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>
@@ -255,6 +277,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))
{
@@ -548,19 +575,28 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return Array.FindIndex(_videoProfilesH264, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase));
}
- else if (string.Equals("hevc", videoCodec, StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals("hevc", videoCodec, StringComparison.OrdinalIgnoreCase))
{
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;
}
public string GetInputPathArgument(EncodingJobInfo state)
{
- var mediaPath = state.MediaPath ?? string.Empty;
-
- return _mediaEncoder.GetInputArgument(mediaPath, state.MediaSource);
+ return state.MediaSource.VideoType switch
+ {
+ VideoType.Dvd => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistVobFiles(state.MediaPath, null).ToList(), state.MediaSource),
+ VideoType.BluRay => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2tsFiles(state.MediaPath).ToList(), state.MediaSource),
+ _ => _mediaEncoder.GetInputArgument(state.MediaPath, state.MediaSource)
+ };
}
/// <summary>
@@ -614,6 +650,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return "flac";
}
+ if (string.Equals(codec, "dts", StringComparison.OrdinalIgnoreCase))
+ {
+ return "dca";
+ }
+
return codec.ToLowerInvariant();
}
@@ -694,28 +735,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())
@@ -736,9 +792,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;
@@ -806,54 +865,58 @@ 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 (isHwTonemapAvailable && IsOpenclFullSupported())
+ if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965)
{
- if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965)
+ if (doOclTonemap && !isVaapiDecoder)
{
- if (!isVaapiDecoder)
- {
- args.Append(GetOpenclDeviceArgs(0, null, VaapiAlias, OpenclAlias));
- filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
- }
+ args.Append(GetOpenclDeviceArgs(0, null, VaapiAlias, OpenclAlias));
+ filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
}
- else if (_mediaEncoder.IsVaapiDeviceAmd)
+ }
+ else if (_mediaEncoder.IsVaapiDeviceAmd)
+ {
+ if (IsVulkanFullSupported()
+ && _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier
+ && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
{
- if (!IsVulkanFullSupported()
- || !_mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier
- || Environment.OSVersion.Version < _minKernelVersionAmdVkFmtModifier)
+ 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.
+ filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias);
+ }
+ else
+ {
+ 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
- {
- // libplacebo wants an explicitly set vulkan filter device.
- args.Append(GetVulkanDeviceArgs(0, null, VaapiAlias, VulkanAlias));
- filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias);
- }
- }
- else
- {
- args.Append(GetOpenclDeviceArgs(0, null, null, OpenclAlias));
- filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
}
}
+ else if (doOclTonemap)
+ {
+ args.Append(GetOpenclDeviceArgs(0, null, null, OpenclAlias));
+ filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
+ }
args.Append(filterDevArgs);
}
@@ -988,8 +1051,18 @@ namespace MediaBrowser.Controller.MediaEncoding
arg.Append(canvasArgs);
}
- arg.Append(" -i ")
- .Append(GetInputPathArgument(state));
+ if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
+ {
+ var tmpConcatPath = Path.Join(options.TranscodingTempPath, state.MediaSource.Id + ".concat");
+ _mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath);
+ arg.Append(" -f concat -safe 0 -i ")
+ .Append(tmpConcatPath);
+ }
+ else
+ {
+ arg.Append(" -i ")
+ .Append(GetInputPathArgument(state));
+ }
// sub2video for external graphical subtitles
if (state.SubtitleStream is not null
@@ -1083,19 +1156,19 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return "-bsf:v h264_mp4toannexb";
}
- else if (IsH265(stream))
+
+ if (IsH265(stream))
{
return "-bsf:v hevc_mp4toannexb";
}
- else if (IsAAC(stream))
+
+ if (IsAAC(stream))
{
// Convert adts header(mpegts) to asc header(mp4).
return "-bsf:a aac_adtstoasc";
}
- else
- {
- return null;
- }
+
+ return null;
}
public static string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer)
@@ -1152,6 +1225,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))
{
@@ -1159,24 +1237,24 @@ 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)
{
return FormattableString.Invariant($" -rc_mode CBR -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
}
- else
- {
- return FormattableString.Invariant($" -rc_mode VBR -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
- }
+
+ return FormattableString.Invariant($" -rc_mode VBR -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
}
return FormattableString.Invariant($" -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
@@ -1186,14 +1264,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";
}
@@ -1203,7 +1290,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";
}
@@ -1315,22 +1402,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)
@@ -1352,14 +1428,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;
@@ -1390,7 +1470,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var param = string.Empty;
// Tutorials: Enable Intel GuC / HuC firmware loading for Low Power Encoding.
- // https://01.org/linuxgraphics/downloads/firmware
+ // https://01.org/group/43/downloads/firmware
// https://wiki.archlinux.org/title/intel_graphics#Enable_GuC_/_HuC_firmware_loading
// Intel Low Power Encoding can save unnecessary CPU-GPU synchronization,
// which will reduce overhead in performance intensive tasks such as 4k transcoding and tonemapping.
@@ -1495,18 +1575,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
@@ -1516,7 +1638,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)
{
@@ -1524,11 +1647,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;
@@ -1556,13 +1679,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;
@@ -1583,9 +1707,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
@@ -1716,6 +1846,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)
@@ -1783,19 +1921,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))
{
@@ -1817,6 +1977,12 @@ 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";
+ }
+
return param;
}
@@ -1895,12 +2061,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;
}
@@ -2034,9 +2200,9 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
- // Video bitrate must fall within requested value
+ // Audio bitrate must fall within requested value
if (request.AudioBitRate.HasValue
- && audioStream.BitDepth.HasValue
+ && audioStream.BitRate.HasValue
&& audioStream.BitRate.Value > request.AudioBitRate.Value)
{
return false;
@@ -2100,14 +2266,20 @@ namespace MediaBrowser.Controller.MediaEncoding
private static double GetVideoBitrateScaleFactor(string codec)
{
+ // hevc & vp9 - 40% more efficient than h.264
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)
- || string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
+ || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
{
return .6;
}
+ // av1 - 50% more efficient than h.264
+ if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
+ {
+ return .5;
+ }
+
return 1;
}
@@ -2115,7 +2287,9 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec);
var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec);
- var scaleFactor = outputScaleFactor / inputScaleFactor;
+
+ // Don't scale the real bitrate lower than the requested bitrate
+ var scaleFactor = Math.Max(outputScaleFactor / inputScaleFactor, 1);
if (bitrate <= 500000)
{
@@ -2137,56 +2311,96 @@ namespace MediaBrowser.Controller.MediaEncoding
return Convert.ToInt32(scaleFactor * bitrate);
}
- public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream)
+ public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream, int? outputAudioChannels)
{
- return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream);
+ return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream, outputAudioChannels);
}
- public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream)
+ public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream, int? outputAudioChannels)
{
if (audioStream is null)
{
return null;
}
- if (audioBitRate.HasValue && string.IsNullOrEmpty(audioCodec))
+ var inputChannels = audioStream.Channels ?? 0;
+ var outputChannels = outputAudioChannels ?? 0;
+ var bitrate = audioBitRate ?? int.MaxValue;
+
+ if (string.IsNullOrEmpty(audioCodec)
+ || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
{
- return Math.Min(384000, audioBitRate.Value);
+ return (inputChannels, outputChannels) switch
+ {
+ (>= 6, >= 6 or 0) => Math.Min(640000, bitrate),
+ (> 0, > 0) => Math.Min(outputChannels * 128000, bitrate),
+ (> 0, _) => Math.Min(inputChannels * 128000, bitrate),
+ (_, _) => Math.Min(384000, bitrate)
+ };
}
- if (audioBitRate.HasValue && !string.IsNullOrEmpty(audioCodec))
+ if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(audioCodec, "dca", StringComparison.OrdinalIgnoreCase))
{
- if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
+ return (inputChannels, outputChannels) switch
{
- if ((audioStream.Channels ?? 0) >= 6)
- {
- return Math.Min(640000, audioBitRate.Value);
- }
+ (>= 6, >= 6 or 0) => Math.Min(768000, bitrate),
+ (> 0, > 0) => Math.Min(outputChannels * 136000, bitrate),
+ (> 0, _) => Math.Min(inputChannels * 136000, bitrate),
+ (_, _) => Math.Min(672000, bitrate)
+ };
+ }
- return Math.Min(384000, audioBitRate.Value);
- }
+ // Empty bitrate area is not allow on iOS
+ // Default audio bitrate to 128K per channel if we don't have codec specific defaults
+ // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
+ return 128000 * (outputAudioChannels ?? audioStream.Channels ?? 2);
+ }
- if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase)
- || string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))
+ public string GetAudioVbrModeParam(string encoder, int bitratePerChannel)
+ {
+ if (string.Equals(encoder, "libfdk_aac", StringComparison.OrdinalIgnoreCase))
+ {
+ return " -vbr:a " + bitratePerChannel switch
{
- if ((audioStream.Channels ?? 0) >= 6)
- {
- return Math.Min(3584000, audioBitRate.Value);
- }
+ < 32000 => "1",
+ < 48000 => "2",
+ < 64000 => "3",
+ < 96000 => "4",
+ _ => "5"
+ };
+ }
- return Math.Min(1536000, audioBitRate.Value);
- }
+ if (string.Equals(encoder, "libmp3lame", StringComparison.OrdinalIgnoreCase))
+ {
+ return " -qscale:a " + bitratePerChannel switch
+ {
+ < 48000 => "8",
+ < 64000 => "6",
+ < 88000 => "4",
+ < 112000 => "2",
+ _ => "0"
+ };
}
- // Empty bitrate area is not allow on iOS
- // Default audio bitrate to 128K if it is not being requested
- // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
- return 128000;
+ if (string.Equals(encoder, "libvorbis", StringComparison.OrdinalIgnoreCase))
+ {
+ return " -qscale:a " + bitratePerChannel switch
+ {
+ < 40000 => "0",
+ < 56000 => "2",
+ < 80000 => "4",
+ < 112000 => "6",
+ _ => "8"
+ };
+ }
+
+ return null;
}
public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions)
@@ -2456,6 +2670,30 @@ namespace MediaBrowser.Controller.MediaEncoding
}
/// <summary>
+ /// Gets the negative map args by filters.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ /// <param name="videoProcessFilters">The videoProcessFilters.</param>
+ /// <returns>System.String.</returns>
+ public string GetNegativeMapArgsByFilters(EncodingJobInfo state, string videoProcessFilters)
+ {
+ string args = string.Empty;
+
+ // http://ffmpeg.org/ffmpeg-all.html#toc-Complex-filtergraphs-1
+ if (state.VideoStream != null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal))
+ {
+ int videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream);
+
+ args += string.Format(
+ CultureInfo.InvariantCulture,
+ "-map -0:{0} ",
+ videoStreamIndex);
+ }
+
+ return args;
+ }
+
+ /// <summary>
/// Determines which stream will be used for playback.
/// </summary>
/// <param name="allStream">All stream.</param>
@@ -2516,8 +2754,8 @@ namespace MediaBrowser.Controller.MediaEncoding
if (outputWidth > maximumWidth || outputHeight > maximumHeight)
{
- var scaleW = (double)maximumWidth / (double)outputWidth;
- var scaleH = (double)maximumHeight / (double)outputHeight;
+ var scaleW = (double)maximumWidth / outputWidth;
+ var scaleH = (double)maximumHeight / outputHeight;
var scale = Math.Min(scaleW, scaleH);
outputWidth = Math.Min(maximumWidth, (int)(outputWidth * scale));
outputHeight = Math.Min(maximumHeight, (int)(outputHeight * scale));
@@ -2664,79 +2902,76 @@ namespace MediaBrowser.Controller.MediaEncoding
widthParam,
heightParam);
}
- else
- {
- return GetFixedSwScaleFilter(threedFormat, requestedWidth.Value, requestedHeight.Value);
- }
+
+ return GetFixedSwScaleFilter(threedFormat, requestedWidth.Value, requestedHeight.Value);
}
// If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size
- else if (requestedMaxWidth.HasValue && requestedMaxHeight.HasValue)
+
+ if (requestedMaxWidth.HasValue && requestedMaxHeight.HasValue)
{
var maxWidthParam = requestedMaxWidth.Value.ToString(CultureInfo.InvariantCulture);
var maxHeightParam = requestedMaxHeight.Value.ToString(CultureInfo.InvariantCulture);
return string.Format(
- CultureInfo.InvariantCulture,
- "scale=trunc(min(max(iw\\,ih*a)\\,min({0}\\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\\,ih)\\,min({0}/a\\,{1}))/2)*2",
- maxWidthParam,
- maxHeightParam,
- scaleVal);
+ CultureInfo.InvariantCulture,
+ "scale=trunc(min(max(iw\\,ih*a)\\,min({0}\\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\\,ih)\\,min({0}/a\\,{1}))/2)*2",
+ maxWidthParam,
+ maxHeightParam,
+ scaleVal);
}
// If a fixed width was requested
- else if (requestedWidth.HasValue)
+ if (requestedWidth.HasValue)
{
if (threedFormat.HasValue)
{
// This method can handle 0 being passed in for the requested height
return GetFixedSwScaleFilter(threedFormat, requestedWidth.Value, 0);
}
- else
- {
- var widthParam = requestedWidth.Value.ToString(CultureInfo.InvariantCulture);
- return string.Format(
- CultureInfo.InvariantCulture,
- "scale={0}:trunc(ow/a/2)*2",
- widthParam);
- }
+ var widthParam = requestedWidth.Value.ToString(CultureInfo.InvariantCulture);
+
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ "scale={0}:trunc(ow/a/2)*2",
+ widthParam);
}
// If a fixed height was requested
- else if (requestedHeight.HasValue)
+ if (requestedHeight.HasValue)
{
var heightParam = requestedHeight.Value.ToString(CultureInfo.InvariantCulture);
return string.Format(
- CultureInfo.InvariantCulture,
- "scale=trunc(oh*a/{1})*{1}:{0}",
- heightParam,
- scaleVal);
+ CultureInfo.InvariantCulture,
+ "scale=trunc(oh*a/{1})*{1}:{0}",
+ heightParam,
+ scaleVal);
}
// If a max width was requested
- else if (requestedMaxWidth.HasValue)
+ if (requestedMaxWidth.HasValue)
{
var maxWidthParam = requestedMaxWidth.Value.ToString(CultureInfo.InvariantCulture);
return string.Format(
- CultureInfo.InvariantCulture,
- "scale=trunc(min(max(iw\\,ih*a)\\,{0})/{1})*{1}:trunc(ow/a/2)*2",
- maxWidthParam,
- scaleVal);
+ CultureInfo.InvariantCulture,
+ "scale=trunc(min(max(iw\\,ih*a)\\,{0})/{1})*{1}:trunc(ow/a/2)*2",
+ maxWidthParam,
+ scaleVal);
}
// If a max height was requested
- else if (requestedMaxHeight.HasValue)
+ if (requestedMaxHeight.HasValue)
{
var maxHeightParam = requestedMaxHeight.Value.ToString(CultureInfo.InvariantCulture);
return string.Format(
- CultureInfo.InvariantCulture,
- "scale=trunc(oh*a/{1})*{1}:min(max(iw/a\\,ih)\\,{0})",
- maxHeightParam,
- scaleVal);
+ CultureInfo.InvariantCulture,
+ "scale=trunc(oh*a/{1})*{1}:min(max(iw/a\\,ih)\\,{0})",
+ maxHeightParam,
+ scaleVal);
}
return string.Empty;
@@ -2810,18 +3045,21 @@ namespace MediaBrowser.Controller.MediaEncoding
"yadif_cuda={0}:-1:0",
doubleRateDeint ? "1" : "0");
}
- else if (hwDeintSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase))
+
+ if (hwDeintSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase))
{
return string.Format(
CultureInfo.InvariantCulture,
"deinterlace_vaapi=rate={0}",
doubleRateDeint ? "field" : "frame");
}
- else if (hwDeintSuffix.Contains("qsv", StringComparison.OrdinalIgnoreCase))
+
+ if (hwDeintSuffix.Contains("qsv", StringComparison.OrdinalIgnoreCase))
{
return "deinterlace_qsv=mode=2";
}
- else if (hwDeintSuffix.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
+
+ if (hwDeintSuffix.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
{
return string.Format(
CultureInfo.InvariantCulture,
@@ -2832,7 +3070,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))
{
@@ -2844,7 +3082,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,
@@ -2852,36 +3091,28 @@ namespace MediaBrowser.Controller.MediaEncoding
options.VppTonemappingBrightness,
options.VppTonemappingContrast);
}
- else 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}";
}
}
@@ -2893,10 +3124,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>
@@ -3297,7 +3598,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
// prefer hwmap to hwdownload on opencl.
- var hwTransferFilter = hasGraphicalSubs ? "hwdownload" : "hwmap";
+ var hwTransferFilter = hasGraphicalSubs ? "hwdownload" : "hwmap=mode=read";
mainFilters.Add(hwTransferFilter);
mainFilters.Add("format=nv12");
}
@@ -3471,7 +3772,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);
@@ -3540,7 +3841,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
// prefer hwmap to hwdownload on opencl.
// qsv hwmap is not fully implemented for the time being.
- mainFilters.Add(isHwmapUsable ? "hwmap" : "hwdownload");
+ mainFilters.Add(isHwmapUsable ? "hwmap=mode=read" : "hwdownload");
mainFilters.Add("format=nv12");
}
@@ -3672,7 +3973,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);
@@ -3698,6 +3999,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var outFormat = doTonemap ? string.Empty : "nv12";
var hwScaleFilter = GetHwScaleFilter(isVaapiDecoder ? "vaapi" : "qsv", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+
+ // allocate extra pool sizes for vaapi vpp
+ if (!string.IsNullOrEmpty(hwScaleFilter) && isVaapiDecoder)
+ {
+ hwScaleFilter += ":extra_hw_frames=24";
+ }
+
// hw scale
mainFilters.Add(hwScaleFilter);
}
@@ -3744,7 +4052,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
// prefer hwmap to hwdownload on opencl/vaapi.
// qsv hwmap is not fully implemented for the time being.
- mainFilters.Add(isHwmapUsable ? "hwmap" : "hwdownload");
+ mainFilters.Add(isHwmapUsable ? "hwmap=mode=read" : "hwdownload");
mainFilters.Add("format=nv12");
}
@@ -3973,6 +4281,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var outFormat = doTonemap ? string.Empty : "nv12";
var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+
+ // allocate extra pool sizes for vaapi vpp
+ if (!string.IsNullOrEmpty(hwScaleFilter))
+ {
+ hwScaleFilter += ":extra_hw_frames=24";
+ }
+
// hw scale
mainFilters.Add(hwScaleFilter);
}
@@ -4014,7 +4329,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
// prefer hwmap to hwdownload on opencl/vaapi.
- mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap");
+ mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap=mode=read");
mainFilters.Add("format=nv12");
}
@@ -4112,7 +4427,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);
@@ -4141,98 +4455,81 @@ 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_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 (Vega/gfx9+).
+ 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 (Vega/gfx9+).
+ mainFilters.Add("hwmap=derive_device=drm");
+ mainFilters.Add("format=drm_prime");
+ 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");
}
@@ -4241,52 +4538,53 @@ 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);
- }
+ // 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_vaapi");
- subFilters.Add("hwmap=derive_device=vulkan");
- subFilters.Add("format=vulkan");
+ subFilters.Add("hwupload=derive_device=vulkan");
+ subFilters.Add("format=vulkan");
- overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0");
- overlayFilters.Add("scale_vulkan=format=nv12");
+ 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 (Vega/gfx9+).
+ overlayFilters.Add("hwmap=derive_device=drm");
+ overlayFilters.Add("format=drm_prime");
+ 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);
}
}
}
@@ -4367,6 +4665,13 @@ namespace MediaBrowser.Controller.MediaEncoding
outFormat = doOclTonemap ? string.Empty : "nv12";
var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
+
+ // allocate extra pool sizes for vaapi vpp
+ if (!string.IsNullOrEmpty(hwScaleFilter))
+ {
+ hwScaleFilter += ":extra_hw_frames=24";
+ }
+
// hw scale
mainFilters.Add(hwScaleFilter);
}
@@ -4703,26 +5008,27 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return videoStream.BitDepth.Value;
}
- else if (string.Equals(videoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoStream.PixelFormat, "yuvj420p", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals(videoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.PixelFormat, "yuvj420p", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase))
{
return 8;
}
- else if (string.Equals(videoStream.PixelFormat, "yuv420p10le", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoStream.PixelFormat, "yuv444p10le", StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals(videoStream.PixelFormat, "yuv420p10le", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.PixelFormat, "yuv444p10le", StringComparison.OrdinalIgnoreCase))
{
return 10;
}
- else if (string.Equals(videoStream.PixelFormat, "yuv420p12le", StringComparison.OrdinalIgnoreCase)
- || string.Equals(videoStream.PixelFormat, "yuv444p12le", StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals(videoStream.PixelFormat, "yuv420p12le", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoStream.PixelFormat, "yuv444p12le", StringComparison.OrdinalIgnoreCase))
{
return 12;
}
- else
- {
- return 8;
- }
+
+ return 8;
}
return 0;
@@ -4744,7 +5050,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// HWA decoders can handle both video files and video folders.
- var videoType = mediaSource.VideoType;
+ var videoType = state.VideoType;
if (videoType != VideoType.VideoFile
&& videoType != VideoType.Iso
&& videoType != VideoType.Dvd
@@ -4885,8 +5191,18 @@ namespace MediaBrowser.Controller.MediaEncoding
var isVideotoolboxSupported = isMacOS && _mediaEncoder.SupportsHwaccel("videotoolbox");
var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase);
+ var ffmpegVersion = _mediaEncoder.EncoderVersion;
+
// Set the av1 codec explicitly to trigger hw accelerator, otherwise libdav1d will be used.
- var isAv1 = string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase);
+ var isAv1 = ffmpegVersion < _minFFmpegImplictHwaccel
+ && string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase);
+
+ // Allow profile mismatch if decoding H.264 baseline with d3d11va and vaapi hwaccels.
+ var profileMismatch = string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(state.VideoStream?.Profile, "baseline", StringComparison.OrdinalIgnoreCase);
+
+ // Disable the extra internal copy in nvdec. We already handle it in filter chain.
+ var nvdecNoInternalCopy = ffmpegVersion >= _minFFmpegHwaUnsafeOutput;
if (bitDepth == 10 && isCodecAvailable)
{
@@ -4912,14 +5228,16 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (isVaapiSupported && isCodecAvailable)
{
- return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
+ return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty)
+ + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
}
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) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty);
+ 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);
}
}
else
@@ -4939,13 +5257,12 @@ namespace MediaBrowser.Controller.MediaEncoding
if (options.EnableEnhancedNvdecDecoder)
{
// set -threads 1 to nvdec decoder explicitly since it doesn't implement threading support.
- return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty);
- }
- else
- {
- // cuvid decoder doesn't have threading issue.
- return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty);
+ return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty)
+ + (nvdecNoInternalCopy ? " -hwaccel_flags +unsafe_output" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty);
}
+
+ // cuvid decoder doesn't have threading issue.
+ return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty);
}
}
@@ -4954,7 +5271,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (isD3d11Supported && isCodecAvailable)
{
- return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
+ return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty)
+ + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
}
}
@@ -4963,9 +5281,11 @@ namespace MediaBrowser.Controller.MediaEncoding
&& isVaapiSupported
&& isCodecAvailable)
{
- return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
+ return " -hwaccel vaapi" + (outputHwSurface ? " -hwaccel_output_format vaapi" : string.Empty)
+ + (profileMismatch ? " -hwaccel_flags +allow_profile_mismatch" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty);
}
+ // Apple videotoolbox
if (string.Equals(options.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)
&& isVideotoolboxSupported
&& isCodecAvailable)
@@ -5300,7 +5620,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// Automatically set thread count
return mustSetThreadCount ? Math.Max(Environment.ProcessorCount - 1, 1) : 0;
}
- else if (threads >= Environment.ProcessorCount)
+
+ if (threads >= Environment.ProcessorCount)
{
return Environment.ProcessorCount;
}
@@ -5585,14 +5906,22 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var inputChannels = audioStream is null ? 6 : audioStream.Channels ?? 6;
+ var shiftAudioCodecs = new List<string>();
if (inputChannels >= 6)
{
- return;
+ // DTS and TrueHD are not supported by HLS
+ // Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
+ shiftAudioCodecs.Add("dca");
+ shiftAudioCodecs.Add("truehd");
+ }
+ else
+ {
+ // Transcoding to 2ch ac3 or eac3 almost always causes a playback failure
+ // Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
+ shiftAudioCodecs.Add("ac3");
+ shiftAudioCodecs.Add("eac3");
}
- // Transcoding to 2ch ac3 almost always causes a playback failure
- // Keep it in the supported codecs list, but shift it to the end of the list so that if transcoding happens, another codec is used
- var shiftAudioCodecs = new[] { "ac3", "eac3" };
if (audioCodecs.All(i => shiftAudioCodecs.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
return;
@@ -5608,19 +5937,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;
@@ -5769,7 +6104,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// video processing filters.
var videoProcessParam = GetVideoProcessingFilterParam(state, encodingOptions, videoCodec);
- args += videoProcessParam;
+ var negativeMapArgs = GetNegativeMapArgsByFilters(state, videoProcessParam);
+
+ args = negativeMapArgs + args + videoProcessParam;
hasCopyTs = videoProcessParam.Contains("copyts", StringComparison.OrdinalIgnoreCase);
@@ -5832,10 +6169,17 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var bitrate = state.OutputAudioBitrate;
-
- if (bitrate.HasValue)
+ if (bitrate.HasValue && !LosslessAudioCodecs.Contains(codec, StringComparison.OrdinalIgnoreCase))
{
- args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
+ var vbrParam = GetAudioVbrModeParam(codec, bitrate.Value / (channels ?? 2));
+ if (encodingOptions.EnableAudioVbr && vbrParam is not null)
+ {
+ args += vbrParam;
+ }
+ else
+ {
+ args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
+ }
}
if (state.OutputAudioSampleRate.HasValue)
@@ -5853,23 +6197,33 @@ namespace MediaBrowser.Controller.MediaEncoding
var audioTranscodeParams = new List<string>();
var bitrate = state.OutputAudioBitrate;
+ var channels = state.OutputAudioChannels;
+ var outputCodec = state.OutputAudioCodec;
- if (bitrate.HasValue)
+ if (bitrate.HasValue && !LosslessAudioCodecs.Contains(outputCodec, StringComparison.OrdinalIgnoreCase))
{
- audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture));
+ var vbrParam = GetAudioVbrModeParam(GetAudioEncoder(state), bitrate.Value / (channels ?? 2));
+ if (encodingOptions.EnableAudioVbr && vbrParam is not null)
+ {
+ audioTranscodeParams.Add(vbrParam);
+ }
+ else
+ {
+ audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture));
+ }
}
- if (state.OutputAudioChannels.HasValue)
+ if (channels.HasValue)
{
audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
}
- if (!string.IsNullOrEmpty(state.OutputAudioCodec))
+ if (!string.IsNullOrEmpty(outputCodec))
{
audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state));
}
- if (!string.Equals(state.OutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
+ if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase))
{
// opus only supports specific sampling rates
var sampleRate = state.OutputAudioSampleRate;
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 bc6207ac5..f830b9f29 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -154,6 +154,14 @@ namespace MediaBrowser.Controller.MediaEncoding
string GetInputArgument(string inputFile, MediaSourceInfo mediaSource);
/// <summary>
+ /// Gets the input argument.
+ /// </summary>
+ /// <param name="inputFiles">The input files.</param>
+ /// <param name="mediaSource">The mediaSource.</param>
+ /// <returns>System.String.</returns>
+ string GetInputArgument(IReadOnlyList<string> inputFiles, MediaSourceInfo mediaSource);
+
+ /// <summary>
/// Gets the input argument for an external subtitle file.
/// </summary>
/// <param name="inputFile">The input file.</param>
@@ -187,5 +195,27 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <param name="path">The path.</param>
/// <param name="pathType">The type of path.</param>
void UpdateEncoderPath(string path, string pathType);
+
+ /// <summary>
+ /// Gets the primary playlist of .vob files.
+ /// </summary>
+ /// <param name="path">The to the .vob files.</param>
+ /// <param name="titleNumber">The title number to start with.</param>
+ /// <returns>A playlist.</returns>
+ IReadOnlyList<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber);
+
+ /// <summary>
+ /// Gets the primary playlist of .m2ts files.
+ /// </summary>
+ /// <param name="path">The to the .m2ts files.</param>
+ /// <returns>A playlist.</returns>
+ IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path);
+
+ /// <summary>
+ /// Generates a FFmpeg concat config for the source.
+ /// </summary>
+ /// <param name="source">The <see cref="MediaSourceInfo"/>.</param>
+ /// <param name="concatFilePath">The path the config should be written to.</param>
+ void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath);
}
}
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index 0524999c7..a07d9b3eb 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;
@@ -169,7 +169,7 @@ namespace MediaBrowser.Controller.Net
if (data is not null)
{
await connection.SendAsync(
- new WebSocketMessage<TReturnDataType>
+ new OutboundWebSocketMessage<TReturnDataType>
{
MessageId = Guid.NewGuid(),
MessageType = Type,
diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
index 4f2492b89..04b333230 100644
--- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs
+++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs
@@ -5,7 +5,6 @@ using System.Net;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Model.Net;
namespace MediaBrowser.Controller.Net
{
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..2d986b7b3 100644
--- a/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs
+++ b/MediaBrowser.Controller/Net/WebSocketMessageInfo.cs
@@ -1,7 +1,5 @@
#nullable disable
-using MediaBrowser.Model.Net;
-
namespace MediaBrowser.Controller.Net
{
/// <summary>
diff --git a/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs b/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs
new file mode 100644
index 000000000..7c35c8010
--- /dev/null
+++ b/MediaBrowser.Controller/Net/WebSocketMessageOfT.cs
@@ -0,0 +1,33 @@
+#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>
+// TODO make this abstract, remove empty ctor.
+public class WebSocketMessage<T> : WebSocketMessage
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="WebSocketMessage{T}"/> class.
+ /// </summary>
+ public 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..ad97796e7
--- /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; }
+}
diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessageOfT.cs b/MediaBrowser.Controller/Net/WebSocketMessages/OutboundWebSocketMessageOfT.cs
new file mode 100644
index 000000000..f09f294b4
--- /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; }
+}
diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
index f6c592070..d1a51c2cf 100644
--- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
+++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
@@ -56,5 +56,19 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="newIndex">The new index.</param>
/// <returns>Task.</returns>
Task MoveItemAsync(string playlistId, string entryId, int newIndex);
+
+ /// <summary>
+ /// Removed all playlists of a user.
+ /// If the playlist is shared, ownership is transferred.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <returns>Task.</returns>
+ Task RemovePlaylistsAsync(Guid userId);
+
+ /// <summary>
+ /// Saves a playlist.
+ /// </summary>
+ /// <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 e6bcc9ea8..498df5ab0 100644
--- a/MediaBrowser.Controller/Playlists/Playlist.cs
+++ b/MediaBrowser.Controller/Playlists/Playlist.cs
@@ -15,6 +15,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Playlists
@@ -33,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]
@@ -232,7 +236,13 @@ namespace MediaBrowser.Controller.Playlists
return base.IsVisible(user);
}
- if (user.Id.Equals(OwnerUserId))
+ if (OpenAccess)
+ {
+ return true;
+ }
+
+ var userId = user.Id;
+ if (userId.Equals(OwnerUserId))
{
return true;
}
@@ -240,10 +250,9 @@ namespace MediaBrowser.Controller.Playlists
var shares = Shares;
if (shares.Length == 0)
{
- return base.IsVisible(user);
+ return false;
}
- var userId = user.Id;
return shares.Any(share => Guid.TryParse(share.UserId, out var id) && id.Equals(userId));
}
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/Providers/ImageRefreshOptions.cs b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
index fd73ed5f8..05b4d43a5 100644
--- a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
+++ b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
@@ -1,6 +1,7 @@
#pragma warning disable CA1819, CS1591
using System;
+using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Model.Entities;
@@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.Providers
public bool ReplaceAllImages { get; set; }
- public ImageType[] ReplaceImages { get; set; }
+ public IReadOnlyList<ImageType> ReplaceImages { get; set; }
public bool IsAutomated { get; set; }
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index eefc5d222..0c4719a0e 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities.Security;
-using Jellyfin.Data.Events;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Session;
diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
index b86e48243..fcfc18a64 100644
--- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
+++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
@@ -1,7 +1,6 @@
#pragma warning disable CS1591
using System;
-using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
index 216494556..dcc06db1e 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs
@@ -533,11 +533,9 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
_logger.LogWarning("Session {SessionId} is seeking to wrong position, correcting.", session.Id);
return;
}
- else
- {
- // Session is ready.
- context.SetBuffering(session, false);
- }
+
+ // Session is ready.
+ context.SetBuffering(session, false);
if (!context.IsBuffering())
{
diff --git a/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs b/MediaBrowser.Controller/SyncPlay/Queue/PlayQueueManager.cs
index ddbfeb8de..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);
@@ -313,17 +313,13 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
return true;
}
- else
- {
- // Restoring playing item.
- SetPlayingItemByPlaylistId(playingItem.PlaylistItemId);
- return false;
- }
- }
- else
- {
+
+ // Restoring playing item.
+ SetPlayingItemByPlaylistId(playingItem.PlaylistItemId);
return false;
}
+
+ return false;
}
/// <summary>
@@ -411,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();
@@ -506,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);
}
@@ -522,36 +518,33 @@ 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))
{
return _shuffledPlaylist;
}
- else
- {
- return _sortedPlaylist;
- }
+
+ return _sortedPlaylist;
}
/// <summary>
/// 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)
{
return null;
}
- else if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
+
+ if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{
return _shuffledPlaylist[PlayingItemIndex];
}
- else
- {
- return _sortedPlaylist[PlayingItemIndex];
- }
+
+ return _sortedPlaylist[PlayingItemIndex];
}
}
}