diff options
| author | Marc Brooks <IDisposable@gmail.com> | 2025-02-03 19:48:59 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-02-03 19:48:59 -0600 |
| commit | e8cbcde02ebd930a5eeb6c95e0875a9e30acb3e8 (patch) | |
| tree | 2ecd43f232012c8f037f4cd6fee4168e46d01aa3 /MediaBrowser.Controller | |
| parent | 6dc61a430ba3a8480399309f277e5debfd6403ba (diff) | |
| parent | d376b5fbc7cf3ae7440a606a9e885d70605956bd (diff) | |
Merge branch 'master' into sort-nfo-data
Diffstat (limited to 'MediaBrowser.Controller')
63 files changed, 1125 insertions, 666 deletions
diff --git a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs index 635e4eb3d..daf4d9631 100644 --- a/MediaBrowser.Controller/Authentication/AuthenticationResult.cs +++ b/MediaBrowser.Controller/Authentication/AuthenticationResult.cs @@ -1,20 +1,31 @@ #nullable disable -#pragma warning disable CS1591 - -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; -namespace MediaBrowser.Controller.Authentication +namespace MediaBrowser.Controller.Authentication; + +/// <summary> +/// A class representing an authentication result. +/// </summary> +public class AuthenticationResult { - public class AuthenticationResult - { - public UserDto User { get; set; } + /// <summary> + /// Gets or sets the user. + /// </summary> + public UserDto User { get; set; } - public SessionInfo SessionInfo { get; set; } + /// <summary> + /// Gets or sets the session info. + /// </summary> + public SessionInfoDto SessionInfo { get; set; } - public string AccessToken { get; set; } + /// <summary> + /// Gets or sets the access token. + /// </summary> + public string AccessToken { get; set; } - public string ServerId { get; set; } - } + /// <summary> + /// Gets or sets the server id. + /// </summary> + public string ServerId { get; set; } } diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs deleted file mode 100644 index c049bb97e..000000000 --- a/MediaBrowser.Controller/Chapters/IChapterManager.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Chapters -{ - /// <summary> - /// Interface IChapterManager. - /// </summary> - public interface IChapterManager - { - /// <summary> - /// Saves the chapters. - /// </summary> - /// <param name="itemId">The item.</param> - /// <param name="chapters">The set of chapters.</param> - void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters); - } -} diff --git a/MediaBrowser.Controller/Chapters/IChapterRepository.cs b/MediaBrowser.Controller/Chapters/IChapterRepository.cs new file mode 100644 index 000000000..e22cb0f58 --- /dev/null +++ b/MediaBrowser.Controller/Chapters/IChapterRepository.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Chapters; + +/// <summary> +/// Interface IChapterManager. +/// </summary> +public interface IChapterRepository +{ + /// <summary> + /// Saves the chapters. + /// </summary> + /// <param name="itemId">The item.</param> + /// <param name="chapters">The set of chapters.</param> + void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters); + + /// <summary> + /// Gets all chapters associated with the baseItem. + /// </summary> + /// <param name="baseItem">The baseitem.</param> + /// <returns>A readonly list of chapter instances.</returns> + IReadOnlyList<ChapterInfo> GetChapters(BaseItemDto baseItem); + + /// <summary> + /// Gets a single chapter of a BaseItem on a specific index. + /// </summary> + /// <param name="baseItem">The baseitem.</param> + /// <param name="index">The index of that chapter.</param> + /// <returns>A chapter instance.</returns> + ChapterInfo? GetChapter(BaseItemDto baseItem, int index); + + /// <summary> + /// Gets all chapters associated with the baseItem. + /// </summary> + /// <param name="baseItemId">The BaseItems id.</param> + /// <returns>A readonly list of chapter instances.</returns> + IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId); + + /// <summary> + /// Gets a single chapter of a BaseItem on a specific index. + /// </summary> + /// <param name="baseItemId">The BaseItems id.</param> + /// <param name="index">The index of that chapter.</param> + /// <returns>A chapter instance.</returns> + ChapterInfo? GetChapter(Guid baseItemId, int index); +} diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 5566421cb..fe7dc1cf9 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -1,81 +1,117 @@ -#nullable disable - -#pragma warning disable CS1591 - using System; using System.Threading.Tasks; +using Jellyfin.Data.Dtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; using MediaBrowser.Model.Devices; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; -namespace MediaBrowser.Controller.Devices +namespace MediaBrowser.Controller.Devices; + +/// <summary> +/// Device manager interface. +/// </summary> +public interface IDeviceManager { - public interface IDeviceManager - { - event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated; + /// <summary> + /// Event handler for updated device options. + /// </summary> + event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated; + + /// <summary> + /// Creates a new device. + /// </summary> + /// <param name="device">The device to create.</param> + /// <returns>A <see cref="Task{Device}"/> representing the creation of the device.</returns> + Task<Device> CreateDevice(Device device); - /// <summary> - /// Creates a new device. - /// </summary> - /// <param name="device">The device to create.</param> - /// <returns>A <see cref="Task{Device}"/> representing the creation of the device.</returns> - Task<Device> CreateDevice(Device device); + /// <summary> + /// Saves the capabilities. + /// </summary> + /// <param name="deviceId">The device id.</param> + /// <param name="capabilities">The capabilities.</param> + void SaveCapabilities(string deviceId, ClientCapabilities capabilities); - /// <summary> - /// Saves the capabilities. - /// </summary> - /// <param name="deviceId">The device id.</param> - /// <param name="capabilities">The capabilities.</param> - void SaveCapabilities(string deviceId, ClientCapabilities capabilities); + /// <summary> + /// Gets the capabilities. + /// </summary> + /// <param name="deviceId">The device id.</param> + /// <returns>ClientCapabilities.</returns> + ClientCapabilities GetCapabilities(string? deviceId); - /// <summary> - /// Gets the capabilities. - /// </summary> - /// <param name="deviceId">The device id.</param> - /// <returns>ClientCapabilities.</returns> - ClientCapabilities GetCapabilities(string deviceId); + /// <summary> + /// Gets the device information. + /// </summary> + /// <param name="id">The identifier.</param> + /// <returns>DeviceInfoDto.</returns> + DeviceInfoDto? GetDevice(string id); - /// <summary> - /// Gets the device information. - /// </summary> - /// <param name="id">The identifier.</param> - /// <returns>DeviceInfo.</returns> - DeviceInfo GetDevice(string id); + /// <summary> + /// Gets devices based on the provided query. + /// </summary> + /// <param name="query">The device query.</param> + /// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns> + QueryResult<Device> GetDevices(DeviceQuery query); - /// <summary> - /// Gets devices based on the provided query. - /// </summary> - /// <param name="query">The device query.</param> - /// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns> - QueryResult<Device> GetDevices(DeviceQuery query); + /// <summary> + /// Gets device information based on the provided query. + /// </summary> + /// <param name="query">The device query.</param> + /// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the device information.</returns> + QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query); - QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query); + /// <summary> + /// Gets the device information. + /// </summary> + /// <param name="userId">The user's id, or <c>null</c>.</param> + /// <returns>IEnumerable<DeviceInfoDto>.</returns> + QueryResult<DeviceInfoDto> GetDevicesForUser(Guid? userId); - /// <summary> - /// Gets the devices. - /// </summary> - /// <param name="userId">The user's id, or <c>null</c>.</param> - /// <returns>IEnumerable<DeviceInfo>.</returns> - QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId); + /// <summary> + /// Deletes a device. + /// </summary> + /// <param name="device">The device.</param> + /// <returns>A <see cref="Task"/> representing the deletion of the device.</returns> + Task DeleteDevice(Device device); - Task DeleteDevice(Device device); + /// <summary> + /// Updates a device. + /// </summary> + /// <param name="device">The device.</param> + /// <returns>A <see cref="Task"/> representing the update of the device.</returns> + Task UpdateDevice(Device device); - Task UpdateDevice(Device device); + /// <summary> + /// Determines whether this instance [can access device] the specified user identifier. + /// </summary> + /// <param name="user">The user to test.</param> + /// <param name="deviceId">The device id to test.</param> + /// <returns>Whether the user can access the device.</returns> + bool CanAccessDevice(User user, string deviceId); - /// <summary> - /// Determines whether this instance [can access device] the specified user identifier. - /// </summary> - /// <param name="user">The user to test.</param> - /// <param name="deviceId">The device id to test.</param> - /// <returns>Whether the user can access the device.</returns> - bool CanAccessDevice(User user, string deviceId); + /// <summary> + /// Updates the options of a device. + /// </summary> + /// <param name="deviceId">The device id.</param> + /// <param name="deviceName">The device name.</param> + /// <returns>A <see cref="Task"/> representing the update of the device options.</returns> + Task UpdateDeviceOptions(string deviceId, string? deviceName); - Task UpdateDeviceOptions(string deviceId, string deviceName); + /// <summary> + /// Gets the options of a device. + /// </summary> + /// <param name="deviceId">The device id.</param> + /// <returns><see cref="DeviceOptions"/> of the device.</returns> + DeviceOptionsDto? GetDeviceOptions(string deviceId); - DeviceOptions GetDeviceOptions(string deviceId); - } + /// <summary> + /// Gets the dto for client capabilities. + /// </summary> + /// <param name="capabilities">The client capabilities.</param> + /// <returns><see cref="ClientCapabilitiesDto"/> of the device.</returns> + ClientCapabilitiesDto ToClientCapabilitiesDto(ClientCapabilities capabilities); } diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index 0d1e2a5a0..702ce39a2 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Drawing @@ -60,11 +61,35 @@ namespace MediaBrowser.Controller.Drawing /// <summary> /// Gets the image cache tag. /// </summary> + /// <param name="baseItemPath">The items basePath.</param> + /// <param name="imageDateModified">The image last modification date.</param> + /// <returns>Guid.</returns> + string? GetImageCacheTag(string baseItemPath, DateTime imageDateModified); + + /// <summary> + /// Gets the image cache tag. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="image">The image.</param> + /// <returns>Guid.</returns> + string? GetImageCacheTag(BaseItemDto item, ChapterInfo image); + + /// <summary> + /// Gets the image cache tag. + /// </summary> /// <param name="item">The item.</param> /// <param name="image">The image.</param> /// <returns>Guid.</returns> string GetImageCacheTag(BaseItem item, ItemImageInfo image); + /// <summary> + /// Gets the image cache tag. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="image">The image.</param> + /// <returns>Guid.</returns> + string GetImageCacheTag(BaseItemDto item, ItemImageInfo image); + string? GetImageCacheTag(BaseItem item, ChapterInfo chapter); string? GetImageCacheTag(User user); diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index 40cdd6c91..a02802f41 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -23,7 +23,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> public class AggregateFolder : Folder { - private readonly object _childIdsLock = new object(); + private readonly Lock _childIdsLock = new(); /// <summary> /// The _virtual children. @@ -64,7 +64,7 @@ namespace MediaBrowser.Controller.Entities return CreateResolveArgs(directoryService, true).FileSystemChildren; } - protected override List<BaseItem> LoadChildren() + protected override IReadOnlyList<BaseItem> LoadChildren() { lock (_childIdsLock) { diff --git a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs index 1625c748a..b085398c5 100644 --- a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs @@ -22,7 +22,7 @@ namespace MediaBrowser.Controller.Entities.Audio IReadOnlyList<string> Artists { get; set; } } - public static class Extentions + public static class Extensions { public static IEnumerable<string> GetAllArtists<T>(this T item) where T : IHasArtist, IHasAlbumArtist diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index a0aae8769..f3873775b 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -21,6 +21,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// <summary> /// Class MusicAlbum. /// </summary> + [Common.RequiresSourceSerialisation] public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<AlbumInfo>, IMetadataContainer { public MusicAlbum() diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 1ab6c9706..ecb3ac3a6 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -21,6 +21,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// <summary> /// Class MusicArtist. /// </summary> + [Common.RequiresSourceSerialisation] public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo> { [JsonIgnore] @@ -84,7 +85,7 @@ namespace MediaBrowser.Controller.Entities.Audio return !IsAccessedByName; } - public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query) { if (query.IncludeItemTypes.Length == 0) { @@ -110,15 +111,15 @@ namespace MediaBrowser.Controller.Entities.Audio return base.IsSaveLocalMetadataEnabled(); } - protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) + protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken) { if (IsAccessedByName) { // Should never get in here anyway - return Task.CompletedTask; + return; } - return base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, false, refreshOptions, directoryService, cancellationToken); + await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, false, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false); } public override List<string> GetUserDataKeys() diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index 7448d02ea..65669e680 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// <summary> /// Class MusicGenre. /// </summary> + [Common.RequiresSourceSerialisation] public class MusicGenre : BaseItem, IItemByName { [JsonIgnore] @@ -64,7 +65,7 @@ namespace MediaBrowser.Controller.Entities.Audio return true; } - public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query) { query.GenreIds = new[] { Id }; query.IncludeItemTypes = new[] { BaseItemKind.MusicVideo, BaseItemKind.Audio, BaseItemKind.MusicAlbum, BaseItemKind.MusicArtist }; diff --git a/MediaBrowser.Controller/Entities/AudioBook.cs b/MediaBrowser.Controller/Entities/AudioBook.cs index 782481fbc..666bf2a75 100644 --- a/MediaBrowser.Controller/Entities/AudioBook.cs +++ b/MediaBrowser.Controller/Entities/AudioBook.cs @@ -9,6 +9,7 @@ using MediaBrowser.Controller.Providers; namespace MediaBrowser.Controller.Entities { + [Common.RequiresSourceSerialisation] public class AudioBook : Audio.Audio, IHasSeries, IHasLookupInfo<SongInfo> { [JsonIgnore] diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 8201ae318..29b0b6861 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.IO; using System.Linq; @@ -16,6 +17,7 @@ using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; @@ -480,6 +482,8 @@ namespace MediaBrowser.Controller.Entities public static IItemRepository ItemRepository { get; set; } + public static IChapterRepository ChapterRepository { get; set; } + public static IFileSystem FileSystem { get; set; } public static IUserDataManager UserDataManager { get; set; } @@ -1042,7 +1046,7 @@ namespace MediaBrowser.Controller.Entities return PlayAccess.Full; } - public virtual List<MediaStream> GetMediaStreams() + public virtual IReadOnlyList<MediaStream> GetMediaStreams() { return MediaSourceManager.GetMediaStreams(new MediaStreamQuery { @@ -1055,7 +1059,7 @@ namespace MediaBrowser.Controller.Entities return false; } - public virtual List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution) + public virtual IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution) { if (SourceType == SourceType.Channel) { @@ -1089,7 +1093,7 @@ namespace MediaBrowser.Controller.Entities return 1; }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0) .ThenByDescending(i => i, new MediaSourceWidthComparator()) - .ToList(); + .ToArray(); } protected virtual IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources() @@ -1609,7 +1613,7 @@ namespace MediaBrowser.Controller.Entities } var parent = GetParents().FirstOrDefault() ?? this; - if (parent is UserRootFolder or AggregateFolder) + if (parent is UserRootFolder or AggregateFolder or UserView) { return true; } @@ -1781,7 +1785,7 @@ namespace MediaBrowser.Controller.Entities } else { - Studios = [..current, name]; + Studios = [.. current, name]; } } } @@ -1795,7 +1799,7 @@ namespace MediaBrowser.Controller.Entities /// Adds a genre to the item. /// </summary> /// <param name="name">The name.</param> - /// <exception cref="ArgumentNullException">Throwns if name is null.</exception> + /// <exception cref="ArgumentNullException">Throws if name is null.</exception> public void AddGenre(string name) { ArgumentException.ThrowIfNullOrEmpty(name); @@ -1803,7 +1807,7 @@ namespace MediaBrowser.Controller.Entities var genres = Genres; if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase)) { - Genres = [..genres, name]; + Genres = [.. genres, name]; } } @@ -1821,7 +1825,10 @@ namespace MediaBrowser.Controller.Entities { ArgumentNullException.ThrowIfNull(user); - var data = UserDataManager.GetUserData(user, this); + var data = UserDataManager.GetUserData(user, this) ?? new UserItemData() + { + Key = GetUserDataKeys().First(), + }; if (datePlayed.HasValue) { @@ -1974,11 +1981,11 @@ namespace MediaBrowser.Controller.Entities public void AddImage(ItemImageInfo image) { - ImageInfos = [..ImageInfos, image]; + ImageInfos = [.. ImageInfos, image]; } - public virtual Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) - => LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken); + public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) + => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false); /// <summary> /// Validates that images within the item are still on the filesystem. @@ -2031,7 +2038,7 @@ namespace MediaBrowser.Controller.Entities { if (imageType == ImageType.Chapter) { - var chapter = ItemRepository.GetChapter(this, imageIndex); + var chapter = ChapterRepository.GetChapter(this.Id, imageIndex); if (chapter is null) { @@ -2081,7 +2088,7 @@ namespace MediaBrowser.Controller.Entities if (image.Type == ImageType.Chapter) { - var chapters = ItemRepository.GetChapters(this); + var chapters = ChapterRepository.GetChapters(this.Id); for (var i = 0; i < chapters.Count; i++) { if (chapters[i].ImagePath == image.Path) @@ -2367,7 +2374,7 @@ namespace MediaBrowser.Controller.Entities } } - protected Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken) + protected async Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken) { var newOptions = new MetadataRefreshOptions(options) { @@ -2428,10 +2435,10 @@ namespace MediaBrowser.Controller.Entities } } - return ownedItem.RefreshMetadata(newOptions, cancellationToken); + await ownedItem.RefreshMetadata(newOptions, cancellationToken).ConfigureAwait(false); } - protected Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken) + protected async Task RefreshMetadataForOwnedVideo(MetadataRefreshOptions options, bool copyTitleMetadata, string path, CancellationToken cancellationToken) { var newOptions = new MetadataRefreshOptions(options) { @@ -2441,9 +2448,7 @@ namespace MediaBrowser.Controller.Entities var id = LibraryManager.GetNewItemId(path, typeof(Video)); // Try to retrieve it from the db. If we don't find it, use the resolved version - var video = LibraryManager.GetItemById(id) as Video; - - if (video is null) + if (LibraryManager.GetItemById(id) is not Video video) { video = LibraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)) as Video; @@ -2452,15 +2457,15 @@ namespace MediaBrowser.Controller.Entities if (video is null) { - return Task.FromResult(true); + return; } if (video.OwnerId.IsEmpty()) { - video.OwnerId = this.Id; + video.OwnerId = Id; } - return RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken); + await RefreshMetadataForOwnedItem(video, copyTitleMetadata, newOptions, cancellationToken).ConfigureAwait(false); } public string GetEtag(User user) @@ -2524,7 +2529,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="children">Media children.</param> /// <returns><c>true</c> if the rating was updated; otherwise <c>false</c>.</returns> - public bool UpdateRatingToItems(IList<BaseItem> children) + public bool UpdateRatingToItems(IReadOnlyList<BaseItem> children) { var currentOfficialRating = OfficialRating; diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index 66dea1084..518766937 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -10,6 +10,7 @@ using MediaBrowser.Controller.Providers; namespace MediaBrowser.Controller.Entities { + [Common.RequiresSourceSerialisation] public class Book : BaseItem, IHasLookupInfo<BookInfo>, IHasSeries { public Book() diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 83c19a54e..c110f4d9f 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Linq; using System.Security; @@ -11,6 +12,7 @@ using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; +using J2N.Collections.Generic.Extensions; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -247,7 +249,7 @@ namespace MediaBrowser.Controller.Entities /// We want this synchronous. /// </summary> /// <returns>Returns children.</returns> - protected virtual List<BaseItem> LoadChildren() + protected virtual IReadOnlyList<BaseItem> LoadChildren() { // logger.LogDebug("Loading children from {0} {1} {2}", GetType().Name, Id, Path); // just load our children from the repo - the library will be validated and maintained in other processes @@ -450,7 +452,7 @@ namespace MediaBrowser.Controller.Entities if (newItems.Count > 0) { - LibraryManager.CreateItems(newItems, this, cancellationToken); + LibraryManager.CreateOrUpdateItems(newItems, this, cancellationToken); } } else @@ -528,13 +530,13 @@ namespace MediaBrowser.Controller.Entities } } - private Task RefreshMetadataRecursive(IList<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) + private async Task RefreshMetadataRecursive(IList<BaseItem> children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress<double> progress, CancellationToken cancellationToken) { - return RunTasks( + await RunTasks( (baseItem, innerProgress) => RefreshChildMetadata(baseItem, refreshOptions, recursive && baseItem.IsFolder, innerProgress, cancellationToken), children, progress, - cancellationToken); + cancellationToken).ConfigureAwait(false); } private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) @@ -575,13 +577,13 @@ namespace MediaBrowser.Controller.Entities /// <param name="progress">The progress.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - private Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken) + private async Task ValidateSubFolders(IList<Folder> children, IDirectoryService directoryService, IProgress<double> progress, CancellationToken cancellationToken) { - return RunTasks( + await RunTasks( (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, true, false, false, null, directoryService, cancellationToken), children, progress, - cancellationToken); + cancellationToken).ConfigureAwait(false); } /// <summary> @@ -659,7 +661,7 @@ namespace MediaBrowser.Controller.Entities /// Get our children from the repo - stubbed for now. /// </summary> /// <returns>IEnumerable{BaseItem}.</returns> - protected List<BaseItem> GetCachedChildren() + protected IReadOnlyList<BaseItem> GetCachedChildren() { return ItemRepository.GetItemList(new InternalItemsQuery { @@ -1240,11 +1242,6 @@ namespace MediaBrowser.Controller.Entities return false; } - if (request.GenreIds.Count > 0) - { - return false; - } - if (request.VideoTypes.Length > 0) { return false; @@ -1283,14 +1280,14 @@ namespace MediaBrowser.Controller.Entities return true; } - public List<BaseItem> GetChildren(User user, bool includeLinkedChildren) + public IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren) { ArgumentNullException.ThrowIfNull(user); return GetChildren(user, includeLinkedChildren, new InternalItemsQuery(user)); } - public virtual List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public virtual IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(user); @@ -1304,7 +1301,7 @@ namespace MediaBrowser.Controller.Entities AddChildren(user, includeLinkedChildren, result, false, query); - return result.Values.ToList(); + return result.Values.ToArray(); } protected virtual IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) @@ -1369,7 +1366,7 @@ namespace MediaBrowser.Controller.Entities } } - public virtual IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) + public virtual IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) { ArgumentNullException.ThrowIfNull(user); @@ -1377,35 +1374,35 @@ namespace MediaBrowser.Controller.Entities AddChildren(user, true, result, true, query); - return result.Values; + return result.Values.ToArray(); } /// <summary> /// Gets the recursive children. /// </summary> /// <returns>IList{BaseItem}.</returns> - public IList<BaseItem> GetRecursiveChildren() + public IReadOnlyList<BaseItem> GetRecursiveChildren() { return GetRecursiveChildren(true); } - public IList<BaseItem> GetRecursiveChildren(bool includeLinkedChildren) + public IReadOnlyList<BaseItem> GetRecursiveChildren(bool includeLinkedChildren) { return GetRecursiveChildren(i => true, includeLinkedChildren); } - public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter) + public IReadOnlyList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter) { return GetRecursiveChildren(filter, true); } - public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter, bool includeLinkedChildren) + public IReadOnlyList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter, bool includeLinkedChildren) { var result = new Dictionary<Guid, BaseItem>(); AddChildrenToList(result, includeLinkedChildren, true, filter); - return result.Values.ToList(); + return result.Values.ToArray(); } /// <summary> @@ -1556,11 +1553,12 @@ namespace MediaBrowser.Controller.Entities /// Gets the linked children. /// </summary> /// <returns>IEnumerable{BaseItem}.</returns> - public IEnumerable<Tuple<LinkedChild, BaseItem>> GetLinkedChildrenInfos() + public IReadOnlyList<Tuple<LinkedChild, BaseItem>> GetLinkedChildrenInfos() { return LinkedChildren .Select(i => new Tuple<LinkedChild, BaseItem>(i, GetLinkedChild(i))) - .Where(i => i.Item2 is not null); + .Where(i => i.Item2 is not null) + .ToArray(); } protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index ddf62dd4c..6ec78a270 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Class Genre. /// </summary> + [Common.RequiresSourceSerialisation] public class Genre : BaseItem, IItemByName { /// <summary> @@ -61,7 +62,7 @@ namespace MediaBrowser.Controller.Entities return false; } - public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query) { query.GenreIds = new[] { Id }; query.ExcludeItemTypes = new[] diff --git a/MediaBrowser.Controller/Entities/IHasMediaSources.cs b/MediaBrowser.Controller/Entities/IHasMediaSources.cs index 90d9bdd2d..ad35494c2 100644 --- a/MediaBrowser.Controller/Entities/IHasMediaSources.cs +++ b/MediaBrowser.Controller/Entities/IHasMediaSources.cs @@ -22,8 +22,8 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="enablePathSubstitution"><c>true</c> to enable path substitution, <c>false</c> to not.</param> /// <returns>A list of media sources.</returns> - List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution); + IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution); - List<MediaStream> GetMediaStreams(); + IReadOnlyList<MediaStream> GetMediaStreams(); } } diff --git a/MediaBrowser.Controller/Entities/IItemByName.cs b/MediaBrowser.Controller/Entities/IItemByName.cs index cac8aa61a..4928bda7a 100644 --- a/MediaBrowser.Controller/Entities/IItemByName.cs +++ b/MediaBrowser.Controller/Entities/IItemByName.cs @@ -9,7 +9,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> public interface IItemByName { - IList<BaseItem> GetTaggedItems(InternalItemsQuery query); + IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query); } public interface IHasDualAccess : IItemByName diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 1461a3680..43f02fb72 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -37,7 +37,6 @@ namespace MediaBrowser.Controller.Entities IncludeItemTypes = Array.Empty<BaseItemKind>(); ItemIds = Array.Empty<Guid>(); MediaTypes = Array.Empty<MediaType>(); - MinSimilarityScore = 20; OfficialRatings = Array.Empty<string>(); OrderBy = Array.Empty<(ItemSortBy, SortOrder)>(); PersonIds = Array.Empty<Guid>(); @@ -71,8 +70,6 @@ namespace MediaBrowser.Controller.Entities public User? User { get; set; } - public BaseItem? SimilarTo { get; set; } - public bool? IsFolder { get; set; } public bool? IsFavorite { get; set; } @@ -295,8 +292,6 @@ namespace MediaBrowser.Controller.Entities public DtoOptions DtoOptions { get; set; } - public int MinSimilarityScore { get; set; } - public string? HasNoAudioTrackWithLanguage { get; set; } public string? HasNoInternalSubtitleTrackWithLanguage { get; set; } diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs index fd5fef3dc..98e4f525f 100644 --- a/MediaBrowser.Controller/Entities/LinkedChild.cs +++ b/MediaBrowser.Controller/Entities/LinkedChild.cs @@ -4,7 +4,6 @@ using System; using System.Globalization; -using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities { @@ -12,7 +11,6 @@ namespace MediaBrowser.Controller.Entities { public LinkedChild() { - Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); } public string Path { get; set; } @@ -21,9 +19,6 @@ namespace MediaBrowser.Controller.Entities public string LibraryItemId { get; set; } - [JsonIgnore] - public string Id { get; set; } - /// <summary> /// Gets or sets the linked item id. /// </summary> @@ -31,6 +26,8 @@ namespace MediaBrowser.Controller.Entities public static LinkedChild Create(BaseItem item) { + ArgumentNullException.ThrowIfNull(item); + var child = new LinkedChild { Path = item.Path, diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index a07187d2f..d0c9f049a 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Entities; @@ -91,7 +92,7 @@ namespace MediaBrowser.Controller.Entities.Movies return Enumerable.Empty<BaseItem>(); } - protected override List<BaseItem> LoadChildren() + protected override IReadOnlyList<BaseItem> LoadChildren() { if (IsLegacyBoxSet) { @@ -99,7 +100,7 @@ namespace MediaBrowser.Controller.Entities.Movies } // Save a trip to the database - return new List<BaseItem>(); + return []; } public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders) @@ -127,16 +128,16 @@ namespace MediaBrowser.Controller.Entities.Movies return LibraryManager.Sort(items, user, new[] { sortBy }, SortOrder.Ascending); } - public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { var children = base.GetChildren(user, includeLinkedChildren, query); - return Sort(children, user).ToList(); + return Sort(children, user).ToArray(); } - public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) + public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) { var children = base.GetRecursiveChildren(user, query); - return Sort(children, user).ToList(); + return Sort(children, user).ToArray(); } public BoxSetInfo GetLookupInfo() diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs index d81860436..24b1843ce 100644 --- a/MediaBrowser.Controller/Entities/PeopleHelper.cs +++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs @@ -10,7 +10,7 @@ namespace MediaBrowser.Controller.Entities { public static class PeopleHelper { - public static void AddPerson(List<PersonInfo> people, PersonInfo person) + public static void AddPerson(ICollection<PersonInfo> people, PersonInfo person) { ArgumentNullException.ThrowIfNull(person); ArgumentException.ThrowIfNullOrEmpty(person.Name); diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index 7f265084f..5cc4d322f 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// This is the full Person object that can be retrieved with all of it's data. /// </summary> + [Common.RequiresSourceSerialisation] public class Person : BaseItem, IItemByName, IHasLookupInfo<PersonLookupInfo> { /// <summary> @@ -62,7 +63,7 @@ namespace MediaBrowser.Controller.Entities return value; } - public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query) { query.PersonIds = new[] { Id }; diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs index 3df0b0b78..0ed870bac 100644 --- a/MediaBrowser.Controller/Entities/PersonInfo.cs +++ b/MediaBrowser.Controller/Entities/PersonInfo.cs @@ -17,8 +17,14 @@ namespace MediaBrowser.Controller.Entities public PersonInfo() { ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + Id = Guid.NewGuid(); } + /// <summary> + /// Gets or Sets the PersonId. + /// </summary> + public Guid Id { get; set; } + public Guid ItemId { get; set; } /// <summary> diff --git a/MediaBrowser.Controller/Entities/PhotoAlbum.cs b/MediaBrowser.Controller/Entities/PhotoAlbum.cs index a7ecb9061..5b31b4f11 100644 --- a/MediaBrowser.Controller/Entities/PhotoAlbum.cs +++ b/MediaBrowser.Controller/Entities/PhotoAlbum.cs @@ -4,6 +4,7 @@ using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities { + [Common.RequiresSourceSerialisation] public class PhotoAlbum : Folder { [JsonIgnore] diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs index a3736a4bf..9103b09a9 100644 --- a/MediaBrowser.Controller/Entities/Studio.cs +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -13,6 +13,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Class Studio. /// </summary> + [Common.RequiresSourceSerialisation] public class Studio : BaseItem, IItemByName { /// <summary> @@ -63,7 +64,7 @@ namespace MediaBrowser.Controller.Entities return true; } - public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query) { query.StudioIds = new[] { Id }; diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 181b9be2b..e3fbe8e4d 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -10,6 +10,7 @@ using System.Text.Json.Serialization; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; +using MediaBrowser.Common; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Querying; @@ -19,6 +20,7 @@ namespace MediaBrowser.Controller.Entities.TV /// <summary> /// Class Season. /// </summary> + [RequiresSourceSerialisation] public class Season : Folder, IHasSeries, IHasLookupInfo<SeasonInfo> { [JsonIgnore] @@ -132,7 +134,7 @@ namespace MediaBrowser.Controller.Entities.TV var series = Series; if (series is not null) { - return series.PresentationUniqueKey + "-" + (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture); + return series.PresentationUniqueKey + "-" + IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture); } } diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index a324f79ef..137d91f1c 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -189,12 +189,12 @@ namespace MediaBrowser.Controller.Entities.TV return list; } - public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { return GetSeasons(user, new DtoOptions(true)); } - public List<BaseItem> GetSeasons(User user, DtoOptions options) + public IReadOnlyList<BaseItem> GetSeasons(User user, DtoOptions options) { var query = new InternalItemsQuery(user) { diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index a687adedd..7ae4a4a2c 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -21,7 +21,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> public class UserRootFolder : Folder { - private readonly object _childIdsLock = new object(); + private readonly Lock _childIdsLock = new(); private List<Guid> _childrenIds = null; /// <summary> @@ -52,7 +52,7 @@ namespace MediaBrowser.Controller.Entities } } - protected override List<BaseItem> LoadChildren() + protected override IReadOnlyList<BaseItem> LoadChildren() { lock (_childIdsLock) { diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index e4fb340f7..f5ca3737c 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -134,7 +134,7 @@ namespace MediaBrowser.Controller.Entities } /// <inheritdoc /> - public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) + public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) { query.SetUser(user); query.Recursive = true; @@ -145,7 +145,7 @@ namespace MediaBrowser.Controller.Entities } /// <inheritdoc /> - protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) + protected override IReadOnlyList<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) { return GetChildren(user, false); } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 420349f35..4ec2e4c0a 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -236,7 +236,7 @@ namespace MediaBrowser.Controller.Entities return ConvertToResult(_libraryManager.GetItemList(query)); } - private QueryResult<BaseItem> ConvertToResult(List<BaseItem> items) + private QueryResult<BaseItem> ConvertToResult(IReadOnlyList<BaseItem> items) { return new QueryResult<BaseItem>(items); } diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs index afdaf448b..37820296c 100644 --- a/MediaBrowser.Controller/Entities/Year.cs +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -13,6 +13,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Class Year. /// </summary> + [Common.RequiresSourceSerialisation] public class Year : BaseItem, IItemByName { [JsonIgnore] @@ -55,7 +56,7 @@ namespace MediaBrowser.Controller.Entities return true; } - public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) + public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query) { if (!int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) { diff --git a/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs index 357ef9406..1542c58b3 100644 --- a/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs +++ b/MediaBrowser.Controller/Events/Authentication/AuthenticationResultEventArgs.cs @@ -1,6 +1,5 @@ using System; using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; namespace MediaBrowser.Controller.Events.Authentication; @@ -29,7 +28,7 @@ public class AuthenticationResultEventArgs : EventArgs /// <summary> /// Gets or sets the session information. /// </summary> - public SessionInfo? SessionInfo { get; set; } + public SessionInfoDto? SessionInfo { get; set; } /// <summary> /// Gets or sets the server id. diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index 7ca508426..e4806109a 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -40,14 +40,14 @@ namespace MediaBrowser.Controller.Extensions public const string FfmpegAnalyzeDurationKey = "FFmpeg:analyzeduration"; /// <summary> - /// The key for the FFmpeg path option. + /// The key for the FFmpeg image extraction performance tradeoff option. /// </summary> - public const string FfmpegPathKey = "ffmpeg"; + public const string FfmpegImgExtractPerfTradeoffKey = "FFmpeg:imgExtractPerfTradeoff"; /// <summary> - /// The key for a setting that indicates whether playlists should allow duplicate entries. + /// The key for the FFmpeg path option. /// </summary> - public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates"; + public const string FfmpegPathKey = "ffmpeg"; /// <summary> /// The key for a setting that indicates whether kestrel should bind to a unix socket. @@ -108,12 +108,12 @@ namespace MediaBrowser.Controller.Extensions => configuration.GetValue<bool>(FfmpegSkipValidationKey); /// <summary> - /// Gets a value indicating whether playlists should allow duplicate entries from the <see cref="IConfiguration"/>. + /// Gets a value indicating whether the server should trade off for performance during FFmpeg image extraction. /// </summary> /// <param name="configuration">The configuration to read the setting from.</param> - /// <returns>True if playlists should allow duplicates, otherwise false.</returns> - public static bool DoPlaylistsAllowDuplicates(this IConfiguration configuration) - => configuration.GetValue<bool>(PlaylistsAllowDuplicatesKey); + /// <returns><c>true</c> if the server should trade off for performance during FFmpeg image extraction, otherwise <c>false</c>.</returns> + public static bool GetFFmpegImgExtractPerfTradeoff(this IConfiguration configuration) + => configuration.GetValue<bool>(FfmpegImgExtractPerfTradeoffKey); /// <summary> /// Gets a value indicating whether kestrel should bind to a unix socket from the <see cref="IConfiguration" />. diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index b802b7e6e..8fcd5f605 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -258,7 +258,7 @@ namespace MediaBrowser.Controller.Library /// <param name="items">Items to create.</param> /// <param name="parent">Parent of new items.</param> /// <param name="cancellationToken">CancellationToken to use for operation.</param> - void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken); + void CreateOrUpdateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken); /// <summary> /// Updates the item. @@ -483,21 +483,21 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="item">The item.</param> /// <returns>List<PersonInfo>.</returns> - List<PersonInfo> GetPeople(BaseItem item); + IReadOnlyList<PersonInfo> GetPeople(BaseItem item); /// <summary> /// Gets the people. /// </summary> /// <param name="query">The query.</param> /// <returns>List<PersonInfo>.</returns> - List<PersonInfo> GetPeople(InternalPeopleQuery query); + IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query); /// <summary> /// Gets the people items. /// </summary> /// <param name="query">The query.</param> /// <returns>List<Person>.</returns> - List<Person> GetPeopleItems(InternalPeopleQuery query); + IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query); /// <summary> /// Updates the people. @@ -513,21 +513,21 @@ namespace MediaBrowser.Controller.Library /// <param name="people">The people.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The async task.</returns> - Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken); + Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellationToken); /// <summary> /// Gets the item ids. /// </summary> /// <param name="query">The query.</param> /// <returns>List<Guid>.</returns> - List<Guid> GetItemIds(InternalItemsQuery query); + IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query); /// <summary> /// Gets the people names. /// </summary> /// <param name="query">The query.</param> /// <returns>List<System.String>.</returns> - List<string> GetPeopleNames(InternalPeopleQuery query); + IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query); /// <summary> /// Queries the items. @@ -553,9 +553,9 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="query">The query.</param> /// <returns>QueryResult<BaseItem>.</returns> - List<BaseItem> GetItemList(InternalItemsQuery query); + IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query); - List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent); + IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent); /// <summary> /// Gets the items. @@ -563,7 +563,7 @@ namespace MediaBrowser.Controller.Library /// <param name="query">The query to use.</param> /// <param name="parents">Items to use for query.</param> /// <returns>List of items.</returns> - List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents); + IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents); /// <summary> /// Gets the items result. diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index 44a1a85e3..eb697268c 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -29,31 +29,31 @@ namespace MediaBrowser.Controller.Library /// </summary> /// <param name="itemId">The item identifier.</param> /// <returns>IEnumerable<MediaStream>.</returns> - List<MediaStream> GetMediaStreams(Guid itemId); + IReadOnlyList<MediaStream> GetMediaStreams(Guid itemId); /// <summary> /// Gets the media streams. /// </summary> /// <param name="query">The query.</param> /// <returns>IEnumerable<MediaStream>.</returns> - List<MediaStream> GetMediaStreams(MediaStreamQuery query); + IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery query); /// <summary> /// Gets the media attachments. /// </summary> /// <param name="itemId">The item identifier.</param> /// <returns>IEnumerable<MediaAttachment>.</returns> - List<MediaAttachment> GetMediaAttachments(Guid itemId); + IReadOnlyList<MediaAttachment> GetMediaAttachments(Guid itemId); /// <summary> /// Gets the media attachments. /// </summary> /// <param name="query">The query.</param> /// <returns>IEnumerable<MediaAttachment>.</returns> - List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query); + IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query); /// <summary> - /// Gets the playack media sources. + /// Gets the playback media sources. /// </summary> /// <param name="item">Item to use.</param> /// <param name="user">User to use for operation.</param> @@ -61,7 +61,7 @@ namespace MediaBrowser.Controller.Library /// <param name="enablePathSubstitution">Option to enable path substitution.</param> /// <param name="cancellationToken">CancellationToken to use for operation.</param> /// <returns>List of media sources wrapped in an awaitable task.</returns> - Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken); + Task<IReadOnlyList<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken); /// <summary> /// Gets the static media sources. @@ -70,7 +70,7 @@ namespace MediaBrowser.Controller.Library /// <param name="enablePathSubstitution">Option to enable path substitution.</param> /// <param name="user">User to use for operation.</param> /// <returns>List of media sources.</returns> - List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null); + IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null); /// <summary> /// Gets the static media source. @@ -123,7 +123,7 @@ namespace MediaBrowser.Controller.Library /// <param name="info">The <see cref="ActiveRecordingInfo"/>.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> /// <returns>A task containing the <see cref="MediaSourceInfo"/>'s for the recording.</returns> - Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken); + Task<IReadOnlyList<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken); /// <summary> /// Closes the media source. diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs index 93073cc79..7ba8fc20c 100644 --- a/MediaBrowser.Controller/Library/IMusicManager.cs +++ b/MediaBrowser.Controller/Library/IMusicManager.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.Controller.Library /// <param name="user">The user to use.</param> /// <param name="dtoOptions">The options to use.</param> /// <returns>List of items.</returns> - List<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions); + IReadOnlyList<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions); /// <summary> /// Gets the instant mix from artist. @@ -26,7 +26,7 @@ namespace MediaBrowser.Controller.Library /// <param name="user">The user to use.</param> /// <param name="dtoOptions">The options to use.</param> /// <returns>List of items.</returns> - List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions); + IReadOnlyList<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions); /// <summary> /// Gets the instant mix from genre. @@ -35,6 +35,6 @@ namespace MediaBrowser.Controller.Library /// <param name="user">The user to use.</param> /// <param name="dtoOptions">The options to use.</param> /// <returns>List of items.</returns> - List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions); + IReadOnlyList<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions); } } diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs index f36fd393f..5a2deda66 100644 --- a/MediaBrowser.Controller/Library/IUserDataManager.cs +++ b/MediaBrowser.Controller/Library/IUserDataManager.cs @@ -44,7 +44,7 @@ namespace MediaBrowser.Controller.Library /// <param name="user">User to use.</param> /// <param name="item">Item to use.</param> /// <returns>User data.</returns> - UserItemData GetUserData(User user, BaseItem item); + UserItemData? GetUserData(User user, BaseItem item); /// <summary> /// Gets the user data dto. @@ -52,7 +52,7 @@ namespace MediaBrowser.Controller.Library /// <param name="item">Item to use.</param> /// <param name="user">User to use.</param> /// <returns>User data dto.</returns> - UserItemDataDto GetUserDataDto(BaseItem item, User user); + UserItemDataDto? GetUserDataDto(BaseItem item, User user); /// <summary> /// Gets the user data dto. @@ -62,7 +62,7 @@ namespace MediaBrowser.Controller.Library /// <param name="user">User to use.</param> /// <param name="options">Dto options to use.</param> /// <returns>User data dto.</returns> - UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options); + UserItemDataDto? GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options); /// <summary> /// Updates playstate for an item and returns true or false indicating if it was played to completion. diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs index 3c2cf8e3d..b10e77e10 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Text.Json.Serialization; @@ -119,13 +120,10 @@ namespace MediaBrowser.Controller.LiveTv return "TvChannel"; } - public IEnumerable<BaseItem> GetTaggedItems() - => Enumerable.Empty<BaseItem>(); + public IEnumerable<BaseItem> GetTaggedItems() => []; - public override List<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution) + public override IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution) { - var list = new List<MediaSourceInfo>(); - var info = new MediaSourceInfo { Id = Id.ToString("N", CultureInfo.InvariantCulture), @@ -138,14 +136,12 @@ namespace MediaBrowser.Controller.LiveTv IsInfiniteStream = RunTimeTicks is null }; - list.Add(info); - - return list; + return [info]; } - public override List<MediaStream> GetMediaStreams() + public override IReadOnlyList<MediaStream> GetMediaStreams() { - return new List<MediaStream>(); + return []; } protected override string GetInternalMetadataPath(string basePath) diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index 2ac6f9963..83944f741 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -18,6 +18,7 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.LiveTv { + [Common.RequiresSourceSerialisation] public class LiveTvProgram : BaseItem, IHasLookupInfo<ItemLookupInfo>, IHasStartDate, IHasProgramAttributes { private const string EmbyServiceName = "Emby"; diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 1ef2eb343..ba4a2a59c 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -8,7 +8,7 @@ <PropertyGroup> <Authors>Jellyfin Contributors</Authors> <PackageId>Jellyfin.Controller</PackageId> - <VersionPrefix>10.10.0</VersionPrefix> + <VersionPrefix>10.11.0</VersionPrefix> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> </PropertyGroup> @@ -34,7 +34,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> + <TargetFramework>net9.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <PublishRepositoryUrl>true</PublishRepositoryUrl> diff --git a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs index f77186e25..20f51ddb7 100644 --- a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs @@ -193,6 +193,8 @@ namespace MediaBrowser.Controller.MediaEncoding public bool EnableAudioVbrEncoding { get; set; } + public bool AlwaysBurnInSubtitleWhenTranscoding { get; set; } + public string GetOption(string qualifier, string name) { var value = GetOption(qualifier + "-" + name); diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 88aa888a1..a9e419df4 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -60,7 +60,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18); private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15); - private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0); + private readonly Version _minFFmpegImplicitHwaccel = 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); @@ -71,6 +71,9 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegAdvancedTonemapMode = new Version(7, 0, 1); private readonly Version _minFFmpegAlteredVaVkInterop = new Version(7, 0, 1); private readonly Version _minFFmpegQsvVppTonemapOption = new Version(7, 0, 1); + private readonly Version _minFFmpegQsvVppOutRangeOption = new Version(7, 0, 1); + private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1); + private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0); private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled); @@ -207,6 +210,14 @@ namespace MediaBrowser.Controller.MediaEncoding { var hwType = encodingOptions.HardwareAccelerationType; + // Only Intel has VA-API MJPEG encoder + if (hwType == HardwareAccelerationType.vaapi + && !(_mediaEncoder.IsVaapiDeviceInteliHD + || _mediaEncoder.IsVaapiDeviceInteli965)) + { + return _defaultMjpegEncoder; + } + if (hwType != HardwareAccelerationType.none && encodingOptions.EnableHardwareEncoding && _mjpegCodecMap.TryGetValue(hwType, out var preferredEncoder) @@ -298,8 +309,7 @@ namespace MediaBrowser.Controller.MediaEncoding private bool IsSwTonemapAvailable(EncodingJobInfo state, EncodingOptions options) { if (state.VideoStream is null - || !options.EnableTonemapping - || GetVideoColorBitDepth(state) != 10 + || GetVideoColorBitDepth(state) < 10 || !_mediaEncoder.SupportsFilter("tonemapx")) { return false; @@ -312,7 +322,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.VideoStream is null || !options.EnableTonemapping - || GetVideoColorBitDepth(state) != 10) + || GetVideoColorBitDepth(state) < 10) { return false; } @@ -354,7 +364,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.VideoStream is null || !options.EnableVppTonemapping - || GetVideoColorBitDepth(state) != 10) + || GetVideoColorBitDepth(state) < 10) { return false; } @@ -377,7 +387,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.VideoStream is null || !options.EnableVideoToolboxTonemapping - || GetVideoColorBitDepth(state) != 10) + || GetVideoColorBitDepth(state) < 10) { return false; } @@ -388,6 +398,25 @@ namespace MediaBrowser.Controller.MediaEncoding && state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.HDR10Plus or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG; } + private bool IsVideoStreamHevcRext(EncodingJobInfo state) + { + var videoStream = state.VideoStream; + if (videoStream is null) + { + return false; + } + + return string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) + && (string.Equals(videoStream.Profile, "Rext", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv420p12le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv422p", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv422p10le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv422p12le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p10le", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.PixelFormat, "yuv444p12le", StringComparison.OrdinalIgnoreCase)); + } + /// <summary> /// Gets the name of the output video codec. /// </summary> @@ -599,49 +628,21 @@ namespace MediaBrowser.Controller.MediaEncoding /// <returns>Codec string.</returns> public string InferAudioCodec(string container) { - var ext = "." + (container ?? string.Empty); - - if (string.Equals(ext, ".mp3", StringComparison.OrdinalIgnoreCase)) - { - return "mp3"; - } - - if (string.Equals(ext, ".aac", StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrWhiteSpace(container)) { + // this may not work, but if the client is that broken we cannot do anything better return "aac"; } - if (string.Equals(ext, ".wma", StringComparison.OrdinalIgnoreCase)) - { - return "wma"; - } - - if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase)) - { - return "vorbis"; - } - - if (string.Equals(ext, ".oga", StringComparison.OrdinalIgnoreCase)) - { - return "vorbis"; - } + var inferredCodec = container.ToLowerInvariant(); - if (string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase)) + return inferredCodec switch { - return "vorbis"; - } - - if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase)) - { - return "vorbis"; - } - - if (string.Equals(ext, ".webma", StringComparison.OrdinalIgnoreCase)) - { - return "vorbis"; - } - - return "copy"; + "ogg" or "oga" or "ogv" or "webm" or "webma" => "opus", + "m4a" or "m4b" or "mp4" or "mov" or "mkv" or "mka" => "aac", + "ts" or "avi" or "flv" or "f4v" or "swf" => "mp3", + _ => inferredCodec + }; } /// <summary> @@ -852,13 +853,15 @@ namespace MediaBrowser.Controller.MediaEncoding options); } - private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string srcDeviceAlias, string alias) + private string GetVaapiDeviceArgs(string renderNodePath, string driver, string kernelDriver, string vendorId, string srcDeviceAlias, string alias) { alias ??= VaapiAlias; + var haveVendorId = !string.IsNullOrEmpty(vendorId) + && _mediaEncoder.EncoderVersion >= _minFFmpegVaapiDeviceVendorId; - // 'renderNodePath' has higher priority than 'kernelDriver' + // Priority: 'renderNodePath' > 'vendorId' > 'kernelDriver' var driverOpts = string.IsNullOrEmpty(renderNodePath) - ? (string.IsNullOrEmpty(kernelDriver) ? string.Empty : ",kernel_driver=" + kernelDriver) + ? (haveVendorId ? $",vendor_id={vendorId}" : (string.IsNullOrEmpty(kernelDriver) ? string.Empty : $",kernel_driver={kernelDriver}")) : renderNodePath; // 'driver' behaves similarly to env LIBVA_DRIVER_NAME @@ -893,7 +896,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (OperatingSystem.IsLinux()) { // derive qsv from vaapi device - return GetVaapiDeviceArgs(renderNodePath, "iHD", "i915", null, VaapiAlias) + arg + "@" + VaapiAlias; + return GetVaapiDeviceArgs(renderNodePath, "iHD", "i915", "0x8086", null, VaapiAlias) + arg + "@" + VaapiAlias; } if (OperatingSystem.IsWindows()) @@ -922,7 +925,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // DVBSUB uses the fixed canvas size 720x576 if (state.SubtitleStream is not null - && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode + && ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream && !string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase)) { @@ -988,14 +991,14 @@ namespace MediaBrowser.Controller.MediaEncoding if (_mediaEncoder.IsVaapiDeviceInteliHD) { - args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "iHD", null, null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "iHD", null, 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(options.VaapiDevice, "i965", null, null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(options.VaapiDevice, "i965", null, null, null, VaapiAlias)); } var filterDevArgs = string.Empty; @@ -1019,7 +1022,7 @@ namespace MediaBrowser.Controller.MediaEncoding && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier) { args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias)); - args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(null, null, null, null, DrmAlias, VaapiAlias)); args.Append(GetVulkanDeviceArgs(0, null, DrmAlias, VulkanAlias)); // libplacebo wants an explicitly set vulkan filter device. @@ -1027,7 +1030,7 @@ namespace MediaBrowser.Controller.MediaEncoding } else { - args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, null, VaapiAlias)); + args.Append(GetVaapiDeviceArgs(options.VaapiDevice, null, null, null, null, VaapiAlias)); filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias); if (doOclTonemap) @@ -1221,7 +1224,7 @@ namespace MediaBrowser.Controller.MediaEncoding // sub2video for external graphical subtitles if (state.SubtitleStream is not null - && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode + && ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleStream.IsExternal) { @@ -1421,7 +1424,13 @@ namespace MediaBrowser.Controller.MediaEncoding var encoderPreset = preset ?? defaultPreset; if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) || isLibX265) { - param += " -preset " + encoderPreset.ToString().ToLowerInvariant(); + var presetString = encoderPreset switch + { + EncoderPreset.auto => EncoderPreset.veryfast.ToString().ToLowerInvariant(), + _ => encoderPreset.ToString().ToLowerInvariant() + }; + + param += " -preset " + presetString; int encodeCrf = encodingOptions.H264Crf; if (isLibX265) @@ -2051,7 +2060,13 @@ namespace MediaBrowser.Controller.MediaEncoding // libx265 only accept level option in -x265-params. // level option may cause libx265 to fail. // libx265 cannot adjust the given level, just throw an error. - param += " -x265-params:0 subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1:no-scenecut=1:no-open-gop=1:no-info=1"; + param += " -x265-params:0 no-scenecut=1:no-open-gop=1:no-info=1"; + + if (encodingOptions.EncoderPreset < EncoderPreset.ultrafast) + { + // The following params are slower than the ultrafast preset, don't use when ultrafast is selected. + param += ":subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1"; + } } if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase) @@ -2186,7 +2201,10 @@ namespace MediaBrowser.Controller.MediaEncoding { var videoFrameRate = videoStream.ReferenceFrameRate; - if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value) + // Add a little tolerance to the framerate check because some videos might record a framerate + // that is slightly greater than the intended framerate, but the device can still play it correctly. + // 0.05 fps tolerance should be safe enough. + if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value + 0.05f) { return false; } @@ -2381,7 +2399,7 @@ namespace MediaBrowser.Controller.MediaEncoding return 1; } - private static int ScaleBitrate(int bitrate, string inputVideoCodec, string outputVideoCodec) + public static int ScaleBitrate(int bitrate, string inputVideoCodec, string outputVideoCodec) { var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec); var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec); @@ -2405,6 +2423,12 @@ namespace MediaBrowser.Controller.MediaEncoding { scaleFactor = Math.Max(scaleFactor, 2); } + else if (bitrate >= 30000000) + { + // Don't scale beyond 30Mbps, it is hardly visually noticeable for most codecs with our prefer speed encoding + // and will cause extremely high bitrate to be used for av1->h264 transcoding that will overload clients and encoders + scaleFactor = 1; + } return Convert.ToInt32(scaleFactor * bitrate); } @@ -2535,7 +2559,7 @@ namespace MediaBrowser.Controller.MediaEncoding } var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive; - if (state.SubtitleStream is not null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode && !isCopyingTimestamps) + if (state.SubtitleStream is not null && state.SubtitleStream.IsTextSubtitleStream && ShouldEncodeSubtitle(state) && !isCopyingTimestamps) { var seconds = TimeSpan.FromTicks(state.StartTimeTicks ?? 0).TotalSeconds; @@ -2652,6 +2676,7 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetFastSeekCommandLineParameter(EncodingJobInfo state, EncodingOptions options, string segmentContainer) { var time = state.BaseRequest.StartTimeTicks ?? 0; + var maxTime = state.RunTimeTicks ?? 0; var seekParam = string.Empty; if (time > 0) @@ -2662,6 +2687,14 @@ namespace MediaBrowser.Controller.MediaEncoding // This will help subtitle syncing. var isHlsRemuxing = state.IsVideoRequest && state.TranscodingType is TranscodingJobType.Hls && IsCopyCodec(state.OutputVideoCodec); var seekTick = isHlsRemuxing ? time + 5000000L : time; + + // Seeking beyond EOF makes no sense in transcoding. Clamp the seekTick value to + // [0, RuntimeTicks - 0.5s], so that the muxer gets packets and avoid error codes. + if (maxTime > 0) + { + seekTick = Math.Clamp(seekTick, 0, Math.Max(maxTime - 5000000L, 0)); + } + seekParam += string.Format(CultureInfo.InvariantCulture, "-ss {0}", _mediaEncoder.GetTimeParameter(seekTick)); if (state.IsVideoRequest) @@ -2736,7 +2769,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.AudioStream.IsExternal) { bool hasExternalGraphicsSubs = state.SubtitleStream is not null - && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode + && ShouldEncodeSubtitle(state) && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream; int externalAudioMapIndex = hasExternalGraphicsSubs ? 2 : 1; @@ -3272,7 +3305,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Empty; } - public string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat) + private string GetHwTonemapFilter(EncodingOptions options, string hwTonemapSuffix, string videoFormat, bool forceFullRange) { if (string.IsNullOrEmpty(hwTonemapSuffix)) { @@ -3282,7 +3315,7 @@ namespace MediaBrowser.Controller.MediaEncoding var args = string.Empty; var algorithm = options.TonemappingAlgorithm.ToString().ToLowerInvariant(); var mode = options.TonemappingMode.ToString().ToLowerInvariant(); - var range = options.TonemappingRange; + var range = forceFullRange ? TonemappingRange.pc : options.TonemappingRange; var rangeString = range.ToString().ToLowerInvariant(); if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase)) @@ -3293,24 +3326,25 @@ namespace MediaBrowser.Controller.MediaEncoding && options.VppTonemappingBrightness >= -100 && options.VppTonemappingBrightness <= 100) { - procampParams += $"=b={options.VppTonemappingBrightness}"; + procampParams += "procamp_vaapi=b={0}"; doVaVppProcamp = true; } if (options.VppTonemappingContrast > 1 && options.VppTonemappingContrast <= 10) { - procampParams += doVaVppProcamp ? ":" : "="; - procampParams += $"c={options.VppTonemappingContrast}"; + procampParams += doVaVppProcamp ? ":c={1}" : "procamp_vaapi=c={1}"; doVaVppProcamp = true; } - args = "{0}tonemap_vaapi=format={1}:p=bt709:t=bt709:m=bt709:extra_hw_frames=32"; + args = procampParams + "{2}tonemap_vaapi=format={3}:p=bt709:t=bt709:m=bt709:extra_hw_frames=32"; return string.Format( CultureInfo.InvariantCulture, args, - doVaVppProcamp ? $"procamp_vaapi{procampParams}," : string.Empty, + options.VppTonemappingBrightness, + options.VppTonemappingContrast, + doVaVppProcamp ? "," : string.Empty, videoFormat ?? "nv12"); } else @@ -3352,7 +3386,7 @@ namespace MediaBrowser.Controller.MediaEncoding rangeString); } - public string GetLibplaceboFilter( + private string GetLibplaceboFilter( EncodingOptions options, string videoFormat, bool doTonemap, @@ -3361,7 +3395,8 @@ namespace MediaBrowser.Controller.MediaEncoding int? requestedWidth, int? requestedHeight, int? requestedMaxWidth, - int? requestedMaxHeight) + int? requestedMaxHeight, + bool forceFullRange) { var (outWidth, outHeight) = GetFixedOutputSize( videoWidth, @@ -3386,7 +3421,7 @@ namespace MediaBrowser.Controller.MediaEncoding var algorithm = options.TonemappingAlgorithm; var algorithmString = "clip"; var mode = options.TonemappingMode; - var range = options.TonemappingRange; + var range = forceFullRange ? TonemappingRange.pc : options.TonemappingRange; if (algorithm == TonemappingAlgorithm.bt2390) { @@ -3397,7 +3432,7 @@ namespace MediaBrowser.Controller.MediaEncoding algorithmString = algorithm.ToString().ToLowerInvariant(); } - tonemapArg = ":tonemapping=" + algorithm + ":peak_detect=0:color_primaries=bt709:color_trc=bt709:colorspace=bt709"; + tonemapArg = $":tonemapping={algorithmString}:peak_detect=0:color_primaries=bt709:color_trc=bt709:colorspace=bt709"; if (range == TonemappingRange.tv || range == TonemappingRange.pc) { @@ -3456,7 +3491,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doToneMap = IsSwTonemapAvailable(state, options); var requireDoviReshaping = doToneMap && state.VideoStream.VideoRangeType == VideoRangeType.DOVI; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; @@ -3497,20 +3532,29 @@ namespace MediaBrowser.Controller.MediaEncoding { // tonemapx requires yuv420p10 input for dovi reshaping, let ffmpeg convert the frame when necessary var tonemapFormat = requireDoviReshaping ? "yuv420p" : outFormat; - - var tonemapArgs = $"tonemapx=tonemap={options.TonemappingAlgorithm}:desat={options.TonemappingDesat}:peak={options.TonemappingPeak}:t=bt709:m=bt709:p=bt709:format={tonemapFormat}"; + var tonemapArgString = "tonemapx=tonemap={0}:desat={1}:peak={2}:t=bt709:m=bt709:p=bt709:format={3}"; if (options.TonemappingParam != 0) { - tonemapArgs += $":param={options.TonemappingParam}"; + tonemapArgString += ":param={4}"; } var range = options.TonemappingRange; if (range == TonemappingRange.tv || range == TonemappingRange.pc) { - tonemapArgs += $":range={options.TonemappingRange}"; + tonemapArgString += ":range={5}"; } + var tonemapArgs = string.Format( + CultureInfo.InvariantCulture, + tonemapArgString, + options.TonemappingAlgorithm, + options.TonemappingDesat, + options.TonemappingPeak, + tonemapFormat, + options.TonemappingParam, + options.TonemappingRange); + mainFilters.Add(tonemapArgs); } else @@ -3569,7 +3613,7 @@ namespace MediaBrowser.Controller.MediaEncoding return GetSwVidFilterChain(state, options, vidEncoder); } - // prefered nvdec/cuvid + cuda filters + nvenc pipeline + // preferred nvdec/cuvid + cuda filters + nvenc pipeline return GetNvidiaVidFiltersPrefered(state, options, vidDecoder, vidEncoder); } @@ -3591,6 +3635,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isNvencEncoder = vidEncoder.Contains("nvenc", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isNvencEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isCuInCuOut = isNvDecoder && isNvencEncoder; var doubleRateDeint = options.DeinterlaceDoubleRate && (state.VideoStream?.ReferenceFrameRate ?? 60) <= 30; @@ -3599,7 +3644,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doCuTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -3609,8 +3654,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doCuTranspose = !string.IsNullOrEmpty(tranposeDir) && _mediaEncoder.SupportsFilter("transpose_cuda"); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doCuTranspose = !string.IsNullOrEmpty(transposeDir) && _mediaEncoder.SupportsFilter("transpose_cuda"); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isNvDecoder && doCuTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -3656,10 +3701,11 @@ namespace MediaBrowser.Controller.MediaEncoding // hw transpose if (doCuTranspose) { - mainFilters.Add($"transpose_cuda=dir={tranposeDir}"); + mainFilters.Add($"transpose_cuda=dir={transposeDir}"); } - var outFormat = doCuTonemap ? string.Empty : "yuv420p"; + var isRext = IsVideoStreamHevcRext(state); + var outFormat = doCuTonemap ? (isRext ? "p010" : string.Empty) : "yuv420p"; var hwScaleFilter = GetHwScaleFilter("scale", "cuda", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); // hw scale mainFilters.Add(hwScaleFilter); @@ -3668,7 +3714,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw tonemap if (doCuTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "cuda", "yuv420p"); + var tonemapFilter = GetHwTonemapFilter(options, "cuda", "yuv420p", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -3775,7 +3821,7 @@ namespace MediaBrowser.Controller.MediaEncoding return GetSwVidFilterChain(state, options, vidEncoder); } - // prefered d3d11va + opencl filters + amf pipeline + // preferred d3d11va + opencl filters + amf pipeline return GetAmdDx11VidFiltersPrefered(state, options, vidDecoder, vidEncoder); } @@ -3797,6 +3843,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isAmfEncoder = vidEncoder.Contains("amf", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isAmfEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isDxInDxOut = isD3d11vaDecoder && isAmfEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); @@ -3804,7 +3851,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doOclTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -3814,8 +3861,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doOclTranspose = !string.IsNullOrEmpty(tranposeDir) + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doOclTranspose = !string.IsNullOrEmpty(transposeDir) && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.TransposeOpenclReversal); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isD3d11vaDecoder && doOclTranspose)); var swpInW = swapWAndH ? inH : inW; @@ -3859,12 +3906,12 @@ namespace MediaBrowser.Controller.MediaEncoding // map from d3d11va to opencl via d3d11-opencl interop. mainFilters.Add("hwmap=derive_device=opencl:mode=read"); - // hw deint <= TODO: finsh the 'yadif_opencl' filter + // hw deint <= TODO: finish the 'yadif_opencl' filter // hw transpose if (doOclTranspose) { - mainFilters.Add($"transpose_opencl=dir={tranposeDir}"); + mainFilters.Add($"transpose_opencl=dir={transposeDir}"); } var outFormat = doOclTonemap ? string.Empty : "nv12"; @@ -3876,7 +3923,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4000,13 +4047,13 @@ namespace MediaBrowser.Controller.MediaEncoding return GetSwVidFilterChain(state, options, vidEncoder); } - // prefered qsv(vaapi) + opencl filters pipeline + // preferred qsv(vaapi) + opencl filters pipeline if (isIntelVaapiOclSupported) { return GetIntelQsvVaapiVidFiltersPrefered(state, options, vidDecoder, vidEncoder); } - // prefered qsv(d3d11) + opencl filters pipeline + // preferred qsv(d3d11) + opencl filters pipeline if (isIntelDx11OclSupported) { return GetIntelQsvDx11VidFiltersPrefered(state, options, vidDecoder, vidEncoder); @@ -4035,6 +4082,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isHwDecoder = isD3d11vaDecoder || isQsvDecoder; var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isQsvEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isQsvInQsvOut = isHwDecoder && isQsvEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); @@ -4044,7 +4092,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doOclTonemap = !doVppTonemap && IsHwTonemapAvailable(state, options); var doTonemap = doVppTonemap || doOclTonemap; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -4054,8 +4102,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doVppTranspose = !string.IsNullOrEmpty(tranposeDir); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVppTranspose = !string.IsNullOrEmpty(transposeDir); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || ((isD3d11vaDecoder || isQsvDecoder) && doVppTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -4077,6 +4125,12 @@ namespace MediaBrowser.Controller.MediaEncoding var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12"); var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + if (isMjpegEncoder && !doOclTonemap) + { + // sw decoder + hw mjpeg encoder + swScaleFilter = string.IsNullOrEmpty(swScaleFilter) ? "scale=out_range=pc" : $"{swScaleFilter}:out_range=pc"; + } + // sw scale mainFilters.Add(swScaleFilter); mainFilters.Add($"format={outFormat}"); @@ -4091,43 +4145,69 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (isD3d11vaDecoder || isQsvDecoder) { + var isRext = IsVideoStreamHevcRext(state); + var twoPassVppTonemap = false; + var doVppFullRangeOut = isMjpegEncoder + && _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppOutRangeOption; + var doVppScaleModeHq = isMjpegEncoder + && _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppScaleModeOption; var doVppProcamp = false; var procampParams = string.Empty; + var procampParamsString = string.Empty; if (doVppTonemap) { + if (isRext) + { + // VPP tonemap requires p010 input + twoPassVppTonemap = true; + } + if (options.VppTonemappingBrightness != 0 && options.VppTonemappingBrightness >= -100 && options.VppTonemappingBrightness <= 100) { - procampParams += $":brightness={options.VppTonemappingBrightness}"; - doVppProcamp = true; + procampParamsString += ":brightness={0}"; + twoPassVppTonemap = doVppProcamp = true; } if (options.VppTonemappingContrast > 1 && options.VppTonemappingContrast <= 10) { - procampParams += $":contrast={options.VppTonemappingContrast}"; - doVppProcamp = true; + procampParamsString += ":contrast={1}"; + twoPassVppTonemap = doVppProcamp = true; } - procampParams += doVppProcamp ? ":procamp=1:async_depth=2" : string.Empty; + if (doVppProcamp) + { + procampParamsString += ":procamp=1:async_depth=2"; + procampParams = string.Format( + CultureInfo.InvariantCulture, + procampParamsString, + options.VppTonemappingBrightness, + options.VppTonemappingContrast); + } } - var outFormat = doOclTonemap ? (doVppTranspose ? "p010" : string.Empty) : "nv12"; - outFormat = (doVppTonemap && doVppProcamp) ? "p010" : outFormat; + var outFormat = doOclTonemap ? ((doVppTranspose || isRext) ? "p010" : string.Empty) : "nv12"; + outFormat = twoPassVppTonemap ? "p010" : outFormat; var swapOutputWandH = doVppTranspose && swapWAndH; - var hwScalePrefix = (doVppTranspose || doVppTonemap) ? "vpp" : "scale"; - var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, "qsv", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var hwScaleFilter = GetHwScaleFilter("vpp", "qsv", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTranspose) { - hwScaleFilter += $":transpose={tranposeDir}"; + hwScaleFilter += $":transpose={transposeDir}"; + } + + if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) + { + hwScaleFilter += (doVppFullRangeOut && !doOclTonemap) ? ":out_range=pc" : string.Empty; + hwScaleFilter += doVppScaleModeHq ? ":scale_mode=hq" : string.Empty; } if (!string.IsNullOrEmpty(hwScaleFilter) && doVppTonemap) { - hwScaleFilter += doVppProcamp ? procampParams : ":tonemap=1"; + hwScaleFilter += doVppProcamp ? procampParams : (twoPassVppTonemap ? string.Empty : ":tonemap=1"); } if (isD3d11vaDecoder) @@ -4151,7 +4231,7 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add(hwScaleFilter); // hw tonemap(w/ procamp) - if (doVppTonemap && doVppProcamp) + if (doVppTonemap && twoPassVppTonemap) { mainFilters.Add("vpp_qsv=tonemap=1:format=nv12:async_depth=2"); } @@ -4172,7 +4252,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4289,6 +4369,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isHwDecoder = isVaapiDecoder || isQsvDecoder; var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isQsvEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isQsvInQsvOut = isHwDecoder && isQsvEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); @@ -4298,7 +4379,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doTonemap = doVaVppTonemap || doOclTonemap; var doDeintH2645 = doDeintH264 || doDeintHevc; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -4308,8 +4389,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doVppTranspose = !string.IsNullOrEmpty(tranposeDir); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVppTranspose = !string.IsNullOrEmpty(transposeDir); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || ((isVaapiDecoder || isQsvDecoder) && doVppTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -4331,6 +4412,12 @@ namespace MediaBrowser.Controller.MediaEncoding var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12"); var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + if (isMjpegEncoder && !doOclTonemap) + { + // sw decoder + hw mjpeg encoder + swScaleFilter = string.IsNullOrEmpty(swScaleFilter) ? "scale=out_range=pc" : $"{swScaleFilter}:out_range=pc"; + } + // sw scale mainFilters.Add(swScaleFilter); mainFilters.Add($"format={outFormat}"); @@ -4346,6 +4433,11 @@ namespace MediaBrowser.Controller.MediaEncoding else if (isVaapiDecoder || isQsvDecoder) { var hwFilterSuffix = isVaapiDecoder ? "vaapi" : "qsv"; + var isRext = IsVideoStreamHevcRext(state); + var doVppFullRangeOut = isMjpegEncoder + && _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppOutRangeOption; + var doVppScaleModeHq = isMjpegEncoder + && _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppScaleModeOption; // INPUT vaapi/qsv surface(vram) // hw deint @@ -4358,17 +4450,23 @@ namespace MediaBrowser.Controller.MediaEncoding // hw transpose(vaapi vpp) if (isVaapiDecoder && doVppTranspose) { - mainFilters.Add($"transpose_vaapi=dir={tranposeDir}"); + mainFilters.Add($"transpose_vaapi=dir={transposeDir}"); } - var outFormat = doOclTonemap ? ((isQsvDecoder && doVppTranspose) ? "p010" : string.Empty) : "nv12"; + var outFormat = doTonemap ? (((isQsvDecoder && doVppTranspose) || isRext) ? "p010" : string.Empty) : "nv12"; var swapOutputWandH = isQsvDecoder && doVppTranspose && swapWAndH; - var hwScalePrefix = (isQsvDecoder && doVppTranspose) ? "vpp" : "scale"; + var hwScalePrefix = isQsvDecoder ? "vpp" : "scale"; var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, hwFilterSuffix, outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); if (!string.IsNullOrEmpty(hwScaleFilter) && isQsvDecoder && doVppTranspose) { - hwScaleFilter += $":transpose={tranposeDir}"; + hwScaleFilter += $":transpose={transposeDir}"; + } + + if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) + { + hwScaleFilter += ((isQsvDecoder && !doVppFullRangeOut) || doOclTonemap) ? string.Empty : ":out_range=pc"; + hwScaleFilter += isQsvDecoder ? (doVppScaleModeHq ? ":scale_mode=hq" : string.Empty) : ":mode=hq"; } // allocate extra pool sizes for vaapi vpp scale @@ -4391,7 +4489,7 @@ namespace MediaBrowser.Controller.MediaEncoding mainFilters.Add("format=vaapi"); } - var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); if (isQsvDecoder) @@ -4411,7 +4509,7 @@ namespace MediaBrowser.Controller.MediaEncoding // ocl tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4563,14 +4661,14 @@ namespace MediaBrowser.Controller.MediaEncoding return swFilterChain; } - // prefered vaapi + opencl filters pipeline + // preferred vaapi + opencl filters pipeline if (_mediaEncoder.IsVaapiDeviceInteliHD) { // Intel iHD path, with extra vpp tonemap and overlay support. return GetIntelVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder); } - // prefered vaapi + vulkan filters pipeline + // preferred vaapi + vulkan filters pipeline if (_mediaEncoder.IsVaapiDeviceAmd && isVaapiVkSupported && _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop @@ -4602,6 +4700,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isVaapiEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isVaInVaOut = isVaapiDecoder && isVaapiEncoder; var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); @@ -4611,7 +4710,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doTonemap = doVaVppTonemap || doOclTonemap; var doDeintH2645 = doDeintH264 || doDeintHevc; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -4621,8 +4720,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doVaVppTranspose = !string.IsNullOrEmpty(tranposeDir); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVaVppTranspose = !string.IsNullOrEmpty(transposeDir); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isVaapiDecoder && doVaVppTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -4644,6 +4743,12 @@ namespace MediaBrowser.Controller.MediaEncoding var outFormat = doOclTonemap ? "yuv420p10le" : "nv12"; var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + if (isMjpegEncoder && !doOclTonemap) + { + // sw decoder + hw mjpeg encoder + swScaleFilter = string.IsNullOrEmpty(swScaleFilter) ? "scale=out_range=pc" : $"{swScaleFilter}:out_range=pc"; + } + // sw scale mainFilters.Add(swScaleFilter); mainFilters.Add($"format={outFormat}"); @@ -4658,6 +4763,8 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (isVaapiDecoder) { + var isRext = IsVideoStreamHevcRext(state); + // INPUT vaapi surface(vram) // hw deint if (doDeintH2645) @@ -4669,12 +4776,18 @@ namespace MediaBrowser.Controller.MediaEncoding // hw transpose if (doVaVppTranspose) { - mainFilters.Add($"transpose_vaapi=dir={tranposeDir}"); + mainFilters.Add($"transpose_vaapi=dir={transposeDir}"); } - var outFormat = doTonemap ? string.Empty : "nv12"; + var outFormat = doTonemap ? (isRext ? "p010" : string.Empty) : "nv12"; var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", outFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) + { + hwScaleFilter += doOclTonemap ? string.Empty : ":out_range=pc"; + hwScaleFilter += ":mode=hq"; + } + // allocate extra pool sizes for vaapi vpp if (!string.IsNullOrEmpty(hwScaleFilter)) { @@ -4688,7 +4801,7 @@ namespace MediaBrowser.Controller.MediaEncoding // vaapi vpp tonemap if (doVaVppTonemap && isVaapiDecoder) { - var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4701,7 +4814,7 @@ namespace MediaBrowser.Controller.MediaEncoding // ocl tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -4825,13 +4938,14 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isVaapiEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); var doVkTonemap = IsVulkanHwTonemapAvailable(state, options); var doDeintH2645 = doDeintH264 || doDeintHevc; - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -4839,8 +4953,8 @@ namespace MediaBrowser.Controller.MediaEncoding || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doVkTranspose = isVaapiDecoder && !string.IsNullOrEmpty(tranposeDir); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVkTranspose = isVaapiDecoder && !string.IsNullOrEmpty(transposeDir); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isVaapiDecoder && doVkTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -4920,6 +5034,12 @@ namespace MediaBrowser.Controller.MediaEncoding // hw scale var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", "nv12", false, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + + if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder && !doVkTonemap) + { + hwScaleFilter += ":out_range=pc:mode=hq"; + } + mainFilters.Add(hwScaleFilter); } } @@ -4927,20 +5047,20 @@ namespace MediaBrowser.Controller.MediaEncoding // vk transpose if (doVkTranspose) { - if (string.Equals(tranposeDir, "reversal", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(transposeDir, "reversal", StringComparison.OrdinalIgnoreCase)) { mainFilters.Add("flip_vulkan"); } else { - mainFilters.Add($"transpose_vulkan=dir={tranposeDir}"); + mainFilters.Add($"transpose_vulkan=dir={transposeDir}"); } } // vk libplacebo if (doVkTonemap || hasSubs) { - var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var libplaceboFilter = GetLibplaceboFilter(options, "bgra", doVkTonemap, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH, isMjpegEncoder); mainFilters.Add(libplaceboFilter); mainFilters.Add("format=vulkan"); } @@ -5055,6 +5175,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !isVaapiEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isVaInVaOut = isVaapiDecoder && isVaapiEncoder; var isi965Driver = _mediaEncoder.IsVaapiDeviceInteli965; var isAmdDriver = _mediaEncoder.IsVaapiDeviceAmd; @@ -5064,7 +5185,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doOclTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; @@ -5091,6 +5212,12 @@ namespace MediaBrowser.Controller.MediaEncoding outFormat = doOclTonemap ? "yuv420p10le" : "nv12"; var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + if (isMjpegEncoder && !doOclTonemap) + { + // sw decoder + hw mjpeg encoder + swScaleFilter = string.IsNullOrEmpty(swScaleFilter) ? "scale=out_range=pc" : $"{swScaleFilter}:out_range=pc"; + } + // sw scale mainFilters.Add(swScaleFilter); mainFilters.Add("format=" + outFormat); @@ -5116,6 +5243,12 @@ namespace MediaBrowser.Controller.MediaEncoding outFormat = doOclTonemap ? string.Empty : "nv12"; var hwScaleFilter = GetHwScaleFilter("scale", "vaapi", outFormat, false, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + if (!string.IsNullOrEmpty(hwScaleFilter) && isMjpegEncoder) + { + hwScaleFilter += doOclTonemap ? string.Empty : ":out_range=pc"; + hwScaleFilter += ":mode=hq"; + } + // allocate extra pool sizes for vaapi vpp if (!string.IsNullOrEmpty(hwScaleFilter)) { @@ -5144,7 +5277,7 @@ namespace MediaBrowser.Controller.MediaEncoding // ocl tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -5270,6 +5403,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase); var isVtDecoder = vidDecoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase); + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var inW = state.VideoStream?.Width; var inH = state.VideoStream?.Height; @@ -5287,8 +5421,8 @@ namespace MediaBrowser.Controller.MediaEncoding var usingHwSurface = isVtDecoder && (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface); var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doVtTranspose = !string.IsNullOrEmpty(tranposeDir) && _mediaEncoder.SupportsFilter("transpose_vt"); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doVtTranspose = !string.IsNullOrEmpty(transposeDir) && _mediaEncoder.SupportsFilter("transpose_vt"); var swapWAndH = Math.Abs(rotation) == 90 && doVtTranspose; var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -5312,7 +5446,7 @@ namespace MediaBrowser.Controller.MediaEncoding var hwScaleFilter = GetHwScaleFilter("scale", "vt", scaleFormat, false, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -5332,7 +5466,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hw transpose if (doVtTranspose) { - mainFilters.Add($"transpose_vt=dir={tranposeDir}"); + mainFilters.Add($"transpose_vt=dir={transposeDir}"); } if (doVtTonemap) @@ -5351,7 +5485,7 @@ namespace MediaBrowser.Controller.MediaEncoding // Metal tonemap if (doMetalTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "videotoolbox", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "videotoolbox", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -5447,7 +5581,7 @@ namespace MediaBrowser.Controller.MediaEncoding return GetSwVidFilterChain(state, options, vidEncoder); } - // prefered rkmpp + rkrga + opencl filters pipeline + // preferred rkmpp + rkrga + opencl filters pipeline if (isRkmppOclSupported) { return GetRkmppVidFiltersPrefered(state, options, vidDecoder, vidEncoder); @@ -5474,6 +5608,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isRkmppEncoder = vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase); var isSwDecoder = !isRkmppDecoder; var isSwEncoder = !isRkmppEncoder; + var isMjpegEncoder = vidEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase); var isDrmInDrmOut = isRkmppDecoder && isRkmppEncoder; var isEncoderSupportAfbc = isRkmppEncoder && (vidEncoder.Contains("h264", StringComparison.OrdinalIgnoreCase) @@ -5484,7 +5619,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doOclTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream != null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs @@ -5494,8 +5629,8 @@ namespace MediaBrowser.Controller.MediaEncoding var subH = state.SubtitleStream?.Height; var rotation = state.VideoStream?.Rotation ?? 0; - var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); - var doRkVppTranspose = !string.IsNullOrEmpty(tranposeDir); + var transposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state); + var doRkVppTranspose = !string.IsNullOrEmpty(transposeDir); var swapWAndH = Math.Abs(rotation) == 90 && (isSwDecoder || (isRkmppDecoder && doRkVppTranspose)); var swpInW = swapWAndH ? inH : inW; var swpInH = swapWAndH ? inW : inH; @@ -5517,6 +5652,12 @@ namespace MediaBrowser.Controller.MediaEncoding var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12"); var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, swpInW, swpInH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + if (isMjpegEncoder && !doOclTonemap) + { + // sw decoder + hw mjpeg encoder + swScaleFilter = string.IsNullOrEmpty(swScaleFilter) ? "scale=out_range=pc" : $"{swScaleFilter}:out_range=pc"; + } + if (!string.IsNullOrEmpty(swScaleFilter)) { swScaleFilter += ":flags=fast_bilinear"; @@ -5540,19 +5681,31 @@ namespace MediaBrowser.Controller.MediaEncoding var isFullAfbcPipeline = isEncoderSupportAfbc && isDrmInDrmOut && !doOclTonemap; var swapOutputWandH = doRkVppTranspose && swapWAndH; - var outFormat = doOclTonemap ? "p010" : "nv12"; - var hwScalePrefix = doRkVppTranspose ? "vpp" : "scale"; - var hwScaleFilter = GetHwScaleFilter(hwScalePrefix, "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); - var hwScaleFilter2 = GetHwScaleFilter(hwScalePrefix, "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var outFormat = doOclTonemap ? "p010" : (isMjpegEncoder ? "bgra" : "nv12"); // RGA only support full range in rgb fmts + var hwScaleFilter = GetHwScaleFilter("vpp", "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var doScaling = GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); if (!hasSubs || doRkVppTranspose || !isFullAfbcPipeline - || !string.IsNullOrEmpty(hwScaleFilter2)) + || !string.IsNullOrEmpty(doScaling)) { + // RGA3 hardware only support (1/8 ~ 8) scaling in each blit operation, + // but in Trickplay there's a case: (3840/320 == 12), enable 2pass for it + if (!string.IsNullOrEmpty(doScaling) + && !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f)) + { + // Vendor provided BSP kernel has an RGA driver bug that causes the output to be corrupted for P010 format. + // Use NV15 instead of P010 to avoid the issue. + // SDR inputs are using BGRA formats already which is not affected. + var intermediateFormat = string.Equals(outFormat, "p010", StringComparison.OrdinalIgnoreCase) ? "nv15" : outFormat; + var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_divisible_by=4:afbc=1"; + mainFilters.Add(hwScaleFilterFirstPass); + } + if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose) { - hwScaleFilter += $":transpose={tranposeDir}"; + hwScaleFilter += $":transpose={transposeDir}"; } // try enabling AFBC to save DDR bandwidth @@ -5569,13 +5722,13 @@ namespace MediaBrowser.Controller.MediaEncoding if (doOclTonemap && isRkmppDecoder) { // map from rkmpp/drm to opencl via drm-opencl interop. - mainFilters.Add("hwmap=derive_device=opencl:mode=read"); + mainFilters.Add("hwmap=derive_device=opencl"); } // ocl tonemap if (doOclTonemap) { - var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12"); + var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder); mainFilters.Add(tonemapFilter); } @@ -5612,7 +5765,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // OUTPUT drm(nv12) surface(gem/dma-heap) // reverse-mapping via drm-opencl interop. - mainFilters.Add("hwmap=derive_device=rkmpp:mode=write:reverse=1"); + mainFilters.Add("hwmap=derive_device=rkmpp:reverse=1"); mainFilters.Add("format=drm_prime"); } } @@ -5686,7 +5839,7 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Empty; } - var hasSubs = state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; @@ -5938,19 +6091,6 @@ namespace MediaBrowser.Controller.MediaEncoding } } - var whichCodec = videoStream.Codec; - if (string.Equals(whichCodec, "avc", StringComparison.OrdinalIgnoreCase)) - { - whichCodec = "h264"; - } - else if (string.Equals(whichCodec, "h265", StringComparison.OrdinalIgnoreCase)) - { - whichCodec = "hevc"; - } - - // Avoid a second attempt if no hardware acceleration is being used - options.HardwareDecodingCodecs = options.HardwareDecodingCodecs.Where(c => !string.Equals(c, whichCodec, StringComparison.OrdinalIgnoreCase)).ToArray(); - // leave blank so ffmpeg will decide return null; } @@ -5974,7 +6114,11 @@ namespace MediaBrowser.Controller.MediaEncoding var decoderName = decoderPrefix + '_' + decoderSuffix; var isCodecAvailable = _mediaEncoder.SupportsDecoder(decoderName) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase); - if (bitDepth == 10 && isCodecAvailable) + + // VideoToolbox decoders have built-in SW fallback + if (bitDepth == 10 + && isCodecAvailable + && (options.HardwareAccelerationType != HardwareAccelerationType.videotoolbox)) { if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase) && options.HardwareDecodingCodecs.Contains("hevc", StringComparison.OrdinalIgnoreCase) @@ -6035,7 +6179,7 @@ namespace MediaBrowser.Controller.MediaEncoding var ffmpegVersion = _mediaEncoder.EncoderVersion; // Set the av1 codec explicitly to trigger hw accelerator, otherwise libdav1d will be used. - var isAv1 = ffmpegVersion < _minFFmpegImplictHwaccel + var isAv1 = ffmpegVersion < _minFFmpegImplicitHwaccel && string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase); // Allow profile mismatch if decoding H.264 baseline with d3d11va and vaapi hwaccels. @@ -6050,17 +6194,40 @@ namespace MediaBrowser.Controller.MediaEncoding && ffmpegVersion >= _minFFmpegDisplayRotationOption; var stripRotationDataArgs = stripRotationData ? " -display_rotation 0" : string.Empty; - if (bitDepth == 10 && isCodecAvailable) + // VideoToolbox decoders have built-in SW fallback + if (isCodecAvailable + && (options.HardwareAccelerationType != HardwareAccelerationType.videotoolbox)) { if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase) - && options.HardwareDecodingCodecs.Contains("hevc", StringComparison.OrdinalIgnoreCase) - && !options.EnableDecodingColorDepth10Hevc) + && options.HardwareDecodingCodecs.Contains("hevc", StringComparison.OrdinalIgnoreCase)) { - return null; + if (IsVideoStreamHevcRext(state)) + { + if (bitDepth <= 10 && !options.EnableDecodingColorDepth10HevcRext) + { + return null; + } + + if (bitDepth == 12 && !options.EnableDecodingColorDepth12HevcRext) + { + return null; + } + + if (hardwareAccelerationType == HardwareAccelerationType.vaapi + && !_mediaEncoder.IsVaapiDeviceInteliHD) + { + return null; + } + } + else if (bitDepth == 10 && !options.EnableDecodingColorDepth10Hevc) + { + return null; + } } if (string.Equals(videoCodec, "vp9", StringComparison.OrdinalIgnoreCase) && options.HardwareDecodingCodecs.Contains("vp9", StringComparison.OrdinalIgnoreCase) + && bitDepth == 10 && !options.EnableDecodingColorDepth10Vp9) { return null; @@ -6172,6 +6339,14 @@ namespace MediaBrowser.Controller.MediaEncoding var is8bitSwFormatsQsv = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsQsv = is8bitSwFormatsQsv || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var is8_10_12bitSwFormatsQsv = is8_10bitSwFormatsQsv + || string.Equals("yuv422p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); // TODO: add more 8/10bit and 4:4:4 formats for Qsv after finishing the ffcheck tool if (is8bitSwFormatsQsv) @@ -6200,12 +6375,6 @@ namespace MediaBrowser.Controller.MediaEncoding if (is8_10bitSwFormatsQsv) { - if (string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) - || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)) - { - return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "qsv", "hevc", bitDepth); - } - if (string.Equals(videoStream.Codec, "vp9", StringComparison.OrdinalIgnoreCase)) { return GetHwaccelType(state, options, "vp9", bitDepth, hwSurface) + GetHwDecoderName(options, "vp9", "qsv", "vp9", bitDepth); @@ -6217,6 +6386,15 @@ namespace MediaBrowser.Controller.MediaEncoding } } + if (is8_10_12bitSwFormatsQsv) + { + if (string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)) + { + return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "qsv", "hevc", bitDepth); + } + } + return null; } @@ -6232,6 +6410,11 @@ namespace MediaBrowser.Controller.MediaEncoding var is8bitSwFormatsNvdec = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsNvdec = is8bitSwFormatsNvdec || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var is8_10_12bitSwFormatsNvdec = is8_10bitSwFormatsNvdec + || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); // TODO: add more 8/10/12bit and 4:4:4 formats for Nvdec after finishing the ffcheck tool if (is8bitSwFormatsNvdec) @@ -6265,12 +6448,6 @@ namespace MediaBrowser.Controller.MediaEncoding if (is8_10bitSwFormatsNvdec) { - if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) - || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) - { - return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "cuvid", "hevc", bitDepth); - } - if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { return GetHwaccelType(state, options, "vp9", bitDepth, hwSurface) + GetHwDecoderName(options, "vp9", "cuvid", "vp9", bitDepth); @@ -6282,6 +6459,15 @@ namespace MediaBrowser.Controller.MediaEncoding } } + if (is8_10_12bitSwFormatsNvdec) + { + if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) + || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + { + return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface) + GetHwDecoderName(options, "hevc", "cuvid", "hevc", bitDepth); + } + } + return null; } @@ -6356,6 +6542,14 @@ namespace MediaBrowser.Controller.MediaEncoding var is8bitSwFormatsVaapi = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsVaapi = is8bitSwFormatsVaapi || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var is8_10_12bitSwFormatsVaapi = is8_10bitSwFormatsVaapi + || string.Equals("yuv422p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); if (is8bitSwFormatsVaapi) { @@ -6383,12 +6577,6 @@ namespace MediaBrowser.Controller.MediaEncoding if (is8_10bitSwFormatsVaapi) { - if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) - || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) - { - return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface); - } - if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { return GetHwaccelType(state, options, "vp9", bitDepth, hwSurface); @@ -6400,6 +6588,15 @@ namespace MediaBrowser.Controller.MediaEncoding } } + if (is8_10_12bitSwFormatsVaapi) + { + if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) + || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + { + return GetHwaccelType(state, options, "hevc", bitDepth, hwSurface); + } + } + return null; } @@ -6414,6 +6611,14 @@ namespace MediaBrowser.Controller.MediaEncoding var is8bitSwFormatsVt = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var is8_10_12bitSwFormatsVt = is8_10bitSwFormatsVt + || string.Equals("yuv422p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) + || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); // The related patches make videotoolbox hardware surface working is only available in jellyfin-ffmpeg 7.0.1 at the moment. bool useHwSurface = (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface) && IsVideoToolboxFullSupported(); @@ -6434,15 +6639,18 @@ namespace MediaBrowser.Controller.MediaEncoding return GetHwaccelType(state, options, "h264", bitDepth, useHwSurface); } - if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) - || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { - return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface); + return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface); } + } - if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + if (is8_10_12bitSwFormatsVt) + { + if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase) + || string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { - return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface); + return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface); } } @@ -6667,7 +6875,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(state.InputVideoSync)) { - inputModifier += " -vsync " + state.InputVideoSync; + inputModifier += GetVideoSyncOption(state.InputVideoSync, _mediaEncoder.EncoderVersion); } if (state.ReadInputAtNativeFramerate && state.InputProtocol != MediaProtocol.Rtsp) @@ -6865,7 +7073,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // 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("dts"); shiftAudioCodecs.Add("truehd"); } else @@ -7052,7 +7260,7 @@ namespace MediaBrowser.Controller.MediaEncoding args += keyFrameArg; - var hasGraphicalSubs = state.SubtitleStream is not null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasGraphicalSubs = state.SubtitleStream is not null && !state.SubtitleStream.IsTextSubtitleStream && ShouldEncodeSubtitle(state); var hasCopyTs = false; @@ -7090,7 +7298,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(state.OutputVideoSync)) { - args += " -vsync " + state.OutputVideoSync; + args += GetVideoSyncOption(state.OutputVideoSync, _mediaEncoder.EncoderVersion); } args += GetOutputFFlags(state); @@ -7257,5 +7465,39 @@ namespace MediaBrowser.Controller.MediaEncoding { return string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase); } + + private static bool ShouldEncodeSubtitle(EncodingJobInfo state) + { + return state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode + || (state.BaseRequest.AlwaysBurnInSubtitleWhenTranscoding && !IsCopyCodec(state.OutputVideoCodec)); + } + + public static string GetVideoSyncOption(string videoSync, Version encoderVersion) + { + if (string.IsNullOrEmpty(videoSync)) + { + return string.Empty; + } + + if (encoderVersion >= new Version(5, 1)) + { + if (int.TryParse(videoSync, CultureInfo.InvariantCulture, out var vsync)) + { + return vsync switch + { + -1 => " -fps_mode auto", + 0 => " -fps_mode passthrough", + 1 => " -fps_mode cfr", + 2 => " -fps_mode vfr", + _ => string.Empty + }; + } + + return string.Empty; + } + + // -vsync is deprecated in FFmpeg 5.1 and will be removed in the future. + return $" -vsync {videoSync}"; + } } } diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs index fefa66cdb..56990d0b8 100644 --- a/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs +++ b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs @@ -12,8 +12,8 @@ namespace MediaBrowser.Controller.MediaEncoding; public sealed class TranscodingJob : IDisposable { private readonly ILogger<TranscodingJob> _logger; - private readonly object _processLock = new(); - private readonly object _timerLock = new(); + private readonly Lock _processLock = new(); + private readonly Lock _timerLock = new(); private Timer? _killTimer; diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs index 010d7edb4..570d2bace 100644 --- a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs +++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs @@ -46,12 +46,22 @@ public interface IMediaSegmentManager Task DeleteSegmentAsync(Guid segmentId); /// <summary> - /// Obtains all segments accociated with the itemId. + /// Obtains all segments associated with the itemId. /// </summary> /// <param name="itemId">The id of the <see cref="BaseItem"/>.</param> - /// <param name="typeFilter">filteres all media segments of the given type to be included. If null all types are included.</param> + /// <param name="typeFilter">filters all media segments of the given type to be included. If null all types are included.</param> + /// <param name="filterByProvider">When set filters the segments to only return those that which providers are currently enabled on their library.</param> /// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns> - Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter); + Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true); + + /// <summary> + /// Obtains all segments associated with the itemId. + /// </summary> + /// <param name="item">The <see cref="BaseItem"/>.</param> + /// <param name="typeFilter">filters all media segments of the given type to be included. If null all types are included.</param> + /// <param name="filterByProvider">When set filters the segments to only return those that which providers are currently enabled on their library.</param> + /// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns> + Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true); /// <summary> /// Gets information about any media segments stored for the given itemId. diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs index 39bb58bef..39bb58bef 100644 --- a/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs +++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs index a47d2fa45..4757bfa30 100644 --- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs +++ b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs @@ -33,7 +33,7 @@ namespace MediaBrowser.Controller.Net SingleWriter = false }); - private readonly object _activeConnectionsLock = new(); + private readonly Lock _activeConnectionsLock = new(); /// <summary> /// The _active connections. diff --git a/MediaBrowser.Controller/Net/IWebSocketConnection.cs b/MediaBrowser.Controller/Net/IWebSocketConnection.cs index bba5a6b85..bdc0f9a10 100644 --- a/MediaBrowser.Controller/Net/IWebSocketConnection.cs +++ b/MediaBrowser.Controller/Net/IWebSocketConnection.cs @@ -24,9 +24,9 @@ namespace MediaBrowser.Controller.Net DateTime LastActivityDate { get; } /// <summary> - /// Gets or sets the date of last Keeplive received. + /// Gets or sets the date of last Keepalive received. /// </summary> - /// <value>The date of last Keeplive received.</value> + /// <value>The date of last Keepalive received.</value> DateTime LastKeepAliveDate { get; set; } /// <summary> diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs index 3504831b8..833074541 100644 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs +++ b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SessionsMessage.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.ComponentModel; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; @@ -8,13 +9,13 @@ namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; /// <summary> /// Sessions message. /// </summary> -public class SessionsMessage : OutboundWebSocketMessage<IReadOnlyList<SessionInfo>> +public class SessionsMessage : OutboundWebSocketMessage<IReadOnlyList<SessionInfoDto>> { /// <summary> /// Initializes a new instance of the <see cref="SessionsMessage"/> class. /// </summary> /// <param name="data">Session info.</param> - public SessionsMessage(IReadOnlyList<SessionInfo> data) + public SessionsMessage(IReadOnlyList<SessionInfoDto> data) : base(data) { } diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 2c52b2b45..afe2d833d 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -7,157 +7,82 @@ using System.Collections.Generic; using System.Threading; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; -namespace MediaBrowser.Controller.Persistence +namespace MediaBrowser.Controller.Persistence; + +/// <summary> +/// Provides an interface to implement an Item repository. +/// </summary> +public interface IItemRepository { /// <summary> - /// Provides an interface to implement an Item repository. + /// Deletes the item. + /// </summary> + /// <param name="id">The identifier.</param> + void DeleteItem(Guid id); + + /// <summary> + /// Saves the items. + /// </summary> + /// <param name="items">The items.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken); + + void SaveImages(BaseItem item); + + /// <summary> + /// Retrieves the item. /// </summary> - public interface IItemRepository : IDisposable - { - /// <summary> - /// Deletes the item. - /// </summary> - /// <param name="id">The identifier.</param> - void DeleteItem(Guid id); - - /// <summary> - /// Saves the items. - /// </summary> - /// <param name="items">The items.</param> - /// <param name="cancellationToken">The cancellation token.</param> - void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken); - - void SaveImages(BaseItem item); - - /// <summary> - /// Retrieves the item. - /// </summary> - /// <param name="id">The id.</param> - /// <returns>BaseItem.</returns> - BaseItem RetrieveItem(Guid id); - - /// <summary> - /// Gets chapters for an item. - /// </summary> - /// <param name="item">The item.</param> - /// <returns>The list of chapter info.</returns> - List<ChapterInfo> GetChapters(BaseItem item); - - /// <summary> - /// Gets a single chapter for an item. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="index">The chapter index.</param> - /// <returns>The chapter info at the specified index.</returns> - ChapterInfo GetChapter(BaseItem item, int index); - - /// <summary> - /// Saves the chapters. - /// </summary> - /// <param name="id">The item id.</param> - /// <param name="chapters">The list of chapters to save.</param> - void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters); - - /// <summary> - /// Gets the media streams. - /// </summary> - /// <param name="query">The query.</param> - /// <returns>IEnumerable{MediaStream}.</returns> - List<MediaStream> GetMediaStreams(MediaStreamQuery query); - - /// <summary> - /// Saves the media streams. - /// </summary> - /// <param name="id">The identifier.</param> - /// <param name="streams">The streams.</param> - /// <param name="cancellationToken">The cancellation token.</param> - void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken); - - /// <summary> - /// Gets the media attachments. - /// </summary> - /// <param name="query">The query.</param> - /// <returns>IEnumerable{MediaAttachment}.</returns> - List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query); - - /// <summary> - /// Saves the media attachments. - /// </summary> - /// <param name="id">The identifier.</param> - /// <param name="attachments">The attachments.</param> - /// <param name="cancellationToken">The cancellation token.</param> - void SaveMediaAttachments(Guid id, IReadOnlyList<MediaAttachment> attachments, CancellationToken cancellationToken); - - /// <summary> - /// Gets the items. - /// </summary> - /// <param name="query">The query.</param> - /// <returns>QueryResult<BaseItem>.</returns> - QueryResult<BaseItem> GetItems(InternalItemsQuery query); - - /// <summary> - /// Gets the item ids list. - /// </summary> - /// <param name="query">The query.</param> - /// <returns>List<Guid>.</returns> - List<Guid> GetItemIdsList(InternalItemsQuery query); - - /// <summary> - /// Gets the people. - /// </summary> - /// <param name="query">The query.</param> - /// <returns>List<PersonInfo>.</returns> - List<PersonInfo> GetPeople(InternalPeopleQuery query); - - /// <summary> - /// Updates the people. - /// </summary> - /// <param name="itemId">The item identifier.</param> - /// <param name="people">The people.</param> - void UpdatePeople(Guid itemId, List<PersonInfo> people); - - /// <summary> - /// Gets the people names. - /// </summary> - /// <param name="query">The query.</param> - /// <returns>List<System.String>.</returns> - List<string> GetPeopleNames(InternalPeopleQuery query); - - /// <summary> - /// Gets the item list. - /// </summary> - /// <param name="query">The query.</param> - /// <returns>List<BaseItem>.</returns> - List<BaseItem> GetItemList(InternalItemsQuery query); - - /// <summary> - /// Updates the inherited values. - /// </summary> - void UpdateInheritedValues(); - - int GetCount(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query); - - QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query); - - List<string> GetMusicGenreNames(); - - List<string> GetStudioNames(); - - List<string> GetGenreNames(); - - List<string> GetAllArtistNames(); - } + /// <param name="id">The id.</param> + /// <returns>BaseItem.</returns> + BaseItem RetrieveItem(Guid id); + + /// <summary> + /// Gets the items. + /// </summary> + /// <param name="filter">The query.</param> + /// <returns>QueryResult<BaseItem>.</returns> + QueryResult<BaseItem> GetItems(InternalItemsQuery filter); + + /// <summary> + /// Gets the item ids list. + /// </summary> + /// <param name="filter">The query.</param> + /// <returns>List<Guid>.</returns> + IReadOnlyList<Guid> GetItemIdsList(InternalItemsQuery filter); + + /// <summary> + /// Gets the item list. + /// </summary> + /// <param name="filter">The query.</param> + /// <returns>List<BaseItem>.</returns> + IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery filter); + + /// <summary> + /// Updates the inherited values. + /// </summary> + void UpdateInheritedValues(); + + int GetCount(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery filter); + + QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery filter); + + IReadOnlyList<string> GetMusicGenreNames(); + + IReadOnlyList<string> GetStudioNames(); + + IReadOnlyList<string> GetGenreNames(); + + IReadOnlyList<string> GetAllArtistNames(); } diff --git a/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs new file mode 100644 index 000000000..6699d3a4d --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IItemTypeLookup.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Data.Enums; +using MediaBrowser.Model.Querying; + +namespace MediaBrowser.Controller.Persistence; + +/// <summary> +/// Provides static lookup data for <see cref="ItemFields"/> and <see cref="BaseItemKind"/> for the domain. +/// </summary> +public interface IItemTypeLookup +{ + /// <summary> + /// Gets all serialisation target types for music related kinds. + /// </summary> + IReadOnlyList<string> MusicGenreTypes { get; } + + /// <summary> + /// Gets mapping for all BaseItemKinds and their expected serialization target. + /// </summary> + IReadOnlyDictionary<BaseItemKind, string> BaseItemKindNames { get; } +} diff --git a/MediaBrowser.Controller/Persistence/IMediaAttachmentRepository.cs b/MediaBrowser.Controller/Persistence/IMediaAttachmentRepository.cs new file mode 100644 index 000000000..4773f4058 --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IMediaAttachmentRepository.cs @@ -0,0 +1,28 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Persistence; + +public interface IMediaAttachmentRepository +{ + /// <summary> + /// Gets the media attachments. + /// </summary> + /// <param name="filter">The query.</param> + /// <returns>IEnumerable{MediaAttachment}.</returns> + IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery filter); + + /// <summary> + /// Saves the media attachments. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="attachments">The attachments.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void SaveMediaAttachments(Guid id, IReadOnlyList<MediaAttachment> attachments, CancellationToken cancellationToken); +} diff --git a/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs b/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs new file mode 100644 index 000000000..665129eaf --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IMediaStreamRepository.cs @@ -0,0 +1,31 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Persistence; + +/// <summary> +/// Provides methods for accessing MediaStreams. +/// </summary> +public interface IMediaStreamRepository +{ + /// <summary> + /// Gets the media streams. + /// </summary> + /// <param name="filter">The query.</param> + /// <returns>IEnumerable{MediaStream}.</returns> + IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery filter); + + /// <summary> + /// Saves the media streams. + /// </summary> + /// <param name="id">The identifier.</param> + /// <param name="streams">The streams.</param> + /// <param name="cancellationToken">The cancellation token.</param> + void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken); +} diff --git a/MediaBrowser.Controller/Persistence/IPeopleRepository.cs b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs new file mode 100644 index 000000000..418289cb4 --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IPeopleRepository.cs @@ -0,0 +1,33 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.Persistence; + +public interface IPeopleRepository +{ + /// <summary> + /// Gets the people. + /// </summary> + /// <param name="filter">The query.</param> + /// <returns>The list of people matching the filter.</returns> + IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery filter); + + /// <summary> + /// Updates the people. + /// </summary> + /// <param name="itemId">The item identifier.</param> + /// <param name="people">The people.</param> + void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people); + + /// <summary> + /// Gets the people names. + /// </summary> + /// <param name="filter">The query.</param> + /// <returns>The list of people names matching the filter.</returns> + IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery filter); +} diff --git a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs b/MediaBrowser.Controller/Persistence/IUserDataRepository.cs deleted file mode 100644 index f2fb2826a..000000000 --- a/MediaBrowser.Controller/Persistence/IUserDataRepository.cs +++ /dev/null @@ -1,55 +0,0 @@ -#nullable disable - -using System; -using System.Collections.Generic; -using System.Threading; -using MediaBrowser.Controller.Entities; - -namespace MediaBrowser.Controller.Persistence -{ - /// <summary> - /// Provides an interface to implement a UserData repository. - /// </summary> - public interface IUserDataRepository : IDisposable - { - /// <summary> - /// Saves the user data. - /// </summary> - /// <param name="userId">The user id.</param> - /// <param name="key">The key.</param> - /// <param name="userData">The user data.</param> - /// <param name="cancellationToken">The cancellation token.</param> - void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken); - - /// <summary> - /// Gets the user data. - /// </summary> - /// <param name="userId">The user id.</param> - /// <param name="key">The key.</param> - /// <returns>The user data.</returns> - UserItemData GetUserData(long userId, string key); - - /// <summary> - /// Gets the user data. - /// </summary> - /// <param name="userId">The user id.</param> - /// <param name="keys">The keys.</param> - /// <returns>The user data.</returns> - UserItemData GetUserData(long userId, List<string> keys); - - /// <summary> - /// Return all user data associated with the given user. - /// </summary> - /// <param name="userId">The user id.</param> - /// <returns>The list of user item data.</returns> - List<UserItemData> GetAllUserData(long userId); - - /// <summary> - /// Save all user data associated with the given user. - /// </summary> - /// <param name="userId">The user id.</param> - /// <param name="userData">The user item data.</param> - /// <param name="cancellationToken">The cancellation token.</param> - void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken); - } -} diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index 038cbd2d6..497c4a511 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -92,8 +92,9 @@ namespace MediaBrowser.Controller.Playlists /// <param name="playlistId">The playlist identifier.</param> /// <param name="entryId">The entry identifier.</param> /// <param name="newIndex">The new index.</param> + /// <param name="callingUserId">The calling user.</param> /// <returns>Task.</returns> - Task MoveItemAsync(string playlistId, string entryId, int newIndex); + Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId); /// <summary> /// Removed all playlists of a user. diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 45aefacf6..bf6871a74 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -137,27 +137,27 @@ namespace MediaBrowser.Controller.Playlists return Task.CompletedTask; } - public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) + public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) { return GetPlayableItems(user, query); } - protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) + protected override IReadOnlyList<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) { return []; } - public override IEnumerable<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) + public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query) { return GetPlayableItems(user, query); } - public IEnumerable<Tuple<LinkedChild, BaseItem>> GetManageableItems() + public IReadOnlyList<Tuple<LinkedChild, BaseItem>> GetManageableItems() { return GetLinkedChildrenInfos(); } - private List<BaseItem> GetPlayableItems(User user, InternalItemsQuery query) + private IReadOnlyList<BaseItem> GetPlayableItems(User user, InternalItemsQuery query) { query ??= new InternalItemsQuery(user); diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs index 38fc5f2cc..0d3a334df 100644 --- a/MediaBrowser.Controller/Providers/IProviderManager.cs +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -77,7 +77,8 @@ namespace MediaBrowser.Controller.Providers Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken); /// <summary> - /// Saves the image. + /// Saves the image by giving the image path on filesystem. + /// This method will remove the image on the source path after saving it to the destination. /// </summary> /// <param name="item">Image to save.</param> /// <param name="source">Source of image.</param> diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs index cfff3eb14..ef69885fc 100644 --- a/MediaBrowser.Controller/Providers/MetadataResult.cs +++ b/MediaBrowser.Controller/Providers/MetadataResult.cs @@ -3,6 +3,7 @@ #pragma warning disable CA1002, CA2227, CS1591 using System.Collections.Generic; +using System.Linq; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; @@ -13,6 +14,7 @@ namespace MediaBrowser.Controller.Providers // Images aren't always used so the allocation is a waste a lot of the time private List<LocalImageInfo> _images; private List<(string Url, ImageType Type)> _remoteImages; + private List<PersonInfo> _people; public MetadataResult() { @@ -21,17 +23,21 @@ namespace MediaBrowser.Controller.Providers public List<LocalImageInfo> Images { - get => _images ??= new List<LocalImageInfo>(); + get => _images ??= []; set => _images = value; } public List<(string Url, ImageType Type)> RemoteImages { - get => _remoteImages ??= new List<(string Url, ImageType Type)>(); + get => _remoteImages ??= []; set => _remoteImages = value; } - public List<PersonInfo> People { get; set; } + public IReadOnlyList<PersonInfo> People + { + get => _people; + set => _people = value?.ToList(); + } public bool HasMetadata { get; set; } @@ -47,7 +53,7 @@ namespace MediaBrowser.Controller.Providers { People ??= new List<PersonInfo>(); - PeopleHelper.AddPerson(People, p); + PeopleHelper.AddPerson(_people, p); } /// <summary> @@ -61,7 +67,7 @@ namespace MediaBrowser.Controller.Providers } else { - People.Clear(); + _people.Clear(); } } } diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 5a47236f9..5dd0413b4 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities.Security; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; using MediaBrowser.Model.SyncPlay; @@ -293,6 +294,17 @@ namespace MediaBrowser.Controller.Session SessionInfo GetSession(string deviceId, string client, string version); /// <summary> + /// Gets all sessions available to a user. + /// </summary> + /// <param name="userId">The session identifier.</param> + /// <param name="deviceId">The device id.</param> + /// <param name="activeWithinSeconds">Active within session limit.</param> + /// <param name="controllableUserToCheck">Filter for sessions remote controllable for this user.</param> + /// <param name="isApiKey">Is the request authenticated with API key.</param> + /// <returns>IReadOnlyList{SessionInfoDto}.</returns> + IReadOnlyList<SessionInfoDto> GetSessions(Guid userId, string deviceId, int? activeWithinSeconds, Guid? controllableUserToCheck, bool isApiKey); + + /// <summary> /// Gets the session by authentication token. /// </summary> /// <param name="token">The token.</param> @@ -312,7 +324,7 @@ namespace MediaBrowser.Controller.Session Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, string appVersion); /// <summary> - /// Logouts the specified access token. + /// Logs out the specified access token. /// </summary> /// <param name="accessToken">The access token.</param> /// <returns>A <see cref="Task"/> representing the log out process.</returns> diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 9e3358818..96783f607 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -1,7 +1,5 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Linq; @@ -27,28 +25,45 @@ namespace MediaBrowser.Controller.Session private readonly ISessionManager _sessionManager; private readonly ILogger _logger; - private readonly object _progressLock = new object(); + private readonly Lock _progressLock = new(); private Timer _progressTimer; private PlaybackProgressInfo _lastProgressInfo; - private bool _disposed = false; + private bool _disposed; + /// <summary> + /// Initializes a new instance of the <see cref="SessionInfo"/> class. + /// </summary> + /// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param> + /// <param name="logger">Instance of <see cref="ILogger"/> interface.</param> public SessionInfo(ISessionManager sessionManager, ILogger logger) { _sessionManager = sessionManager; _logger = logger; - AdditionalUsers = Array.Empty<SessionUserInfo>(); + AdditionalUsers = []; PlayState = new PlayerStateInfo(); - SessionControllers = Array.Empty<ISessionController>(); - NowPlayingQueue = Array.Empty<QueueItem>(); - NowPlayingQueueFullItems = Array.Empty<BaseItemDto>(); + SessionControllers = []; + NowPlayingQueue = []; + NowPlayingQueueFullItems = []; } + /// <summary> + /// Gets or sets the play state. + /// </summary> + /// <value>The play state.</value> public PlayerStateInfo PlayState { get; set; } - public SessionUserInfo[] AdditionalUsers { get; set; } + /// <summary> + /// Gets or sets the additional users. + /// </summary> + /// <value>The additional users.</value> + public IReadOnlyList<SessionUserInfo> AdditionalUsers { get; set; } + /// <summary> + /// Gets or sets the client capabilities. + /// </summary> + /// <value>The client capabilities.</value> public ClientCapabilities Capabilities { get; set; } /// <summary> @@ -67,7 +82,7 @@ namespace MediaBrowser.Controller.Session { if (Capabilities is null) { - return Array.Empty<MediaType>(); + return []; } return Capabilities.PlayableMediaTypes; @@ -134,9 +149,17 @@ namespace MediaBrowser.Controller.Session /// <value>The now playing item.</value> public BaseItemDto NowPlayingItem { get; set; } + /// <summary> + /// Gets or sets the now playing queue full items. + /// </summary> + /// <value>The now playing queue full items.</value> [JsonIgnore] public BaseItem FullNowPlayingItem { get; set; } + /// <summary> + /// Gets or sets the now viewing item. + /// </summary> + /// <value>The now viewing item.</value> public BaseItemDto NowViewingItem { get; set; } /// <summary> @@ -156,8 +179,12 @@ namespace MediaBrowser.Controller.Session /// </summary> /// <value>The session controller.</value> [JsonIgnore] - public ISessionController[] SessionControllers { get; set; } + public IReadOnlyList<ISessionController> SessionControllers { get; set; } + /// <summary> + /// Gets or sets the transcoding info. + /// </summary> + /// <value>The transcoding info.</value> public TranscodingInfo TranscodingInfo { get; set; } /// <summary> @@ -177,7 +204,7 @@ namespace MediaBrowser.Controller.Session } } - if (controllers.Length > 0) + if (controllers.Count > 0) { return false; } @@ -186,6 +213,10 @@ namespace MediaBrowser.Controller.Session } } + /// <summary> + /// Gets a value indicating whether the session supports media control. + /// </summary> + /// <value><c>true</c> if this session supports media control; otherwise, <c>false</c>.</value> public bool SupportsMediaControl { get @@ -208,6 +239,10 @@ namespace MediaBrowser.Controller.Session } } + /// <summary> + /// Gets a value indicating whether the session supports remote control. + /// </summary> + /// <value><c>true</c> if this session supports remote control; otherwise, <c>false</c>.</value> public bool SupportsRemoteControl { get @@ -230,16 +265,40 @@ namespace MediaBrowser.Controller.Session } } + /// <summary> + /// Gets or sets the now playing queue. + /// </summary> + /// <value>The now playing queue.</value> public IReadOnlyList<QueueItem> NowPlayingQueue { get; set; } + /// <summary> + /// Gets or sets the now playing queue full items. + /// </summary> + /// <value>The now playing queue full items.</value> public IReadOnlyList<BaseItemDto> NowPlayingQueueFullItems { get; set; } + /// <summary> + /// Gets or sets a value indicating whether the session has a custom device name. + /// </summary> + /// <value><c>true</c> if this session has a custom device name; otherwise, <c>false</c>.</value> public bool HasCustomDeviceName { get; set; } + /// <summary> + /// Gets or sets the playlist item id. + /// </summary> + /// <value>The playlist item id.</value> public string PlaylistItemId { get; set; } + /// <summary> + /// Gets or sets the server id. + /// </summary> + /// <value>The server id.</value> public string ServerId { get; set; } + /// <summary> + /// Gets or sets the user primary image tag. + /// </summary> + /// <value>The user primary image tag.</value> public string UserPrimaryImageTag { get; set; } /// <summary> @@ -247,8 +306,14 @@ namespace MediaBrowser.Controller.Session /// </summary> /// <value>The supported commands.</value> public IReadOnlyList<GeneralCommandType> SupportedCommands - => Capabilities is null ? Array.Empty<GeneralCommandType>() : Capabilities.SupportedCommands; + => Capabilities is null ? [] : Capabilities.SupportedCommands; + /// <summary> + /// Ensures a controller of type exists. + /// </summary> + /// <typeparam name="T">Class to register.</typeparam> + /// <param name="factory">The factory.</param> + /// <returns>Tuple{ISessionController, bool}.</returns> public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory) { var controllers = SessionControllers.ToList(); @@ -261,18 +326,27 @@ namespace MediaBrowser.Controller.Session } var newController = factory(this); - _logger.LogDebug("Creating new {0}", newController.GetType().Name); + _logger.LogDebug("Creating new {Factory}", newController.GetType().Name); controllers.Add(newController); - SessionControllers = controllers.ToArray(); + SessionControllers = [.. controllers]; return new Tuple<ISessionController, bool>(newController, true); } + /// <summary> + /// Adds a controller to the session. + /// </summary> + /// <param name="controller">The controller.</param> public void AddController(ISessionController controller) { - SessionControllers = [..SessionControllers, controller]; + SessionControllers = [.. SessionControllers, controller]; } + /// <summary> + /// Gets a value indicating whether the session contains a user. + /// </summary> + /// <param name="userId">The user id to check.</param> + /// <returns><c>true</c> if this session contains the user; otherwise, <c>false</c>.</returns> public bool ContainsUser(Guid userId) { if (UserId.Equals(userId)) @@ -291,6 +365,11 @@ namespace MediaBrowser.Controller.Session return false; } + /// <summary> + /// Starts automatic progressing. + /// </summary> + /// <param name="progressInfo">The playback progress info.</param> + /// <value>The supported commands.</value> public void StartAutomaticProgress(PlaybackProgressInfo progressInfo) { if (_disposed) @@ -359,6 +438,9 @@ namespace MediaBrowser.Controller.Session } } + /// <summary> + /// Stops automatic progressing. + /// </summary> public void StopAutomaticProgress() { lock (_progressLock) @@ -373,6 +455,10 @@ namespace MediaBrowser.Controller.Session } } + /// <summary> + /// Disposes the instance async. + /// </summary> + /// <returns>ValueTask.</returns> public async ValueTask DisposeAsync() { _disposed = true; @@ -380,7 +466,7 @@ namespace MediaBrowser.Controller.Session StopAutomaticProgress(); var controllers = SessionControllers.ToList(); - SessionControllers = Array.Empty<ISessionController>(); + SessionControllers = []; foreach (var controller in controllers) { diff --git a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs index bd47db39a..66a0c5254 100644 --- a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs +++ b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs @@ -5,7 +5,7 @@ using MediaBrowser.Controller.Library; namespace MediaBrowser.Controller.Sorting { /// <summary> - /// Represents a BaseItem comparer that requires a User to perform it's comparison. + /// Represents a BaseItem comparer that requires a User to perform its comparison. /// </summary> public interface IUserBaseItemComparer : IBaseItemComparer { diff --git a/MediaBrowser.Controller/Streaming/StreamState.cs b/MediaBrowser.Controller/Streaming/StreamState.cs index b5dbe29ec..195dda5fe 100644 --- a/MediaBrowser.Controller/Streaming/StreamState.cs +++ b/MediaBrowser.Controller/Streaming/StreamState.cs @@ -51,7 +51,7 @@ public class StreamState : EncodingJobInfo, IDisposable public VideoRequestDto? VideoRequest => Request as VideoRequestDto; /// <summary> - /// Gets or sets the direct stream provicer. + /// Gets or sets the direct stream provider. /// </summary> /// <remarks> /// Deprecated. diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs index bda794aa6..800317800 100644 --- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs +++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs @@ -46,8 +46,10 @@ public interface ITrickplayManager /// <summary> /// Gets the item ids of all items with trickplay info. /// </summary> + /// <param name="limit">The limit of items to return.</param> + /// <param name="offset">The offset to start the query at.</param> /// <returns>The list of item ids that have trickplay info.</returns> - public Task<IReadOnlyList<Guid>> GetTrickplayItemsAsync(); + Task<IReadOnlyList<TrickplayInfo>> GetTrickplayItemsAsync(int limit, int offset); /// <summary> /// Saves trickplay info. |
