diff options
9 files changed, 602 insertions, 614 deletions
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index bcdf2934a..01b6e31e9 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -90,6 +90,13 @@ namespace MediaBrowser.Controller.MediaEncoding { "truehd", 6 }, }; + private static readonly string _defaultMjpegEncoder = "mjpeg"; + private static readonly Dictionary<string, string> _mjpegCodecMap = new(StringComparer.OrdinalIgnoreCase) + { + { "vaapi", _defaultMjpegEncoder + "_vaapi" }, + { "qsv", _defaultMjpegEncoder + "_qsv" } + }; + public static readonly string[] LosslessAudioCodecs = new string[] { "alac", @@ -151,32 +158,20 @@ namespace MediaBrowser.Controller.MediaEncoding private string GetMjpegEncoder(EncodingJobInfo state, EncodingOptions encodingOptions) { - var defaultEncoder = "mjpeg"; - if (state.VideoType == VideoType.VideoFile) { var hwType = encodingOptions.HardwareAccelerationType; - var codecMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) - { - { "vaapi", defaultEncoder + "_vaapi" }, - { "qsv", defaultEncoder + "_qsv" } - }; - if (!string.IsNullOrEmpty(hwType) && encodingOptions.EnableHardwareEncoding - && codecMap.ContainsKey(hwType)) + && _mjpegCodecMap.TryGetValue(hwType, out var preferredEncoder) + && _mediaEncoder.SupportsEncoder(preferredEncoder)) { - var preferredEncoder = codecMap[hwType]; - - if (_mediaEncoder.SupportsEncoder(preferredEncoder)) - { - return preferredEncoder; - } + return preferredEncoder; } } - return defaultEncoder; + return _defaultMjpegEncoder; } private bool IsVaapiSupported(EncodingJobInfo state) diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs index bae458f98..8e82c57d4 100644 --- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs +++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs @@ -5,50 +5,49 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Controller.Trickplay +namespace MediaBrowser.Controller.Trickplay; + +/// <summary> +/// Interface ITrickplayManager. +/// </summary> +public interface ITrickplayManager { /// <summary> - /// Interface ITrickplayManager. + /// Generate or replace trickplay data. /// </summary> - public interface ITrickplayManager - { - /// <summary> - /// Generate or replace trickplay data. - /// </summary> - /// <param name="video">The video.</param> - /// <param name="replace">Whether or not existing data should be replaced.</param> - /// <param name="cancellationToken">CancellationToken to use for operation.</param> - /// <returns>Task.</returns> - Task RefreshTrickplayData(Video video, bool replace, CancellationToken cancellationToken); + /// <param name="video">The video.</param> + /// <param name="replace">Whether or not existing data should be replaced.</param> + /// <param name="cancellationToken">CancellationToken to use for operation.</param> + /// <returns>Task.</returns> + Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken); - /// <summary> - /// Get available trickplay resolutions and corresponding info. - /// </summary> - /// <param name="itemId">The item.</param> - /// <returns>Map of width resolutions to trickplay tiles info.</returns> - Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId); + /// <summary> + /// Get available trickplay resolutions and corresponding info. + /// </summary> + /// <param name="itemId">The item.</param> + /// <returns>Map of width resolutions to trickplay tiles info.</returns> + Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId); - /// <summary> - /// Saves trickplay tiles info. - /// </summary> - /// <param name="itemId">The item.</param> - /// <param name="tilesInfo">The trickplay tiles info.</param> - void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); + /// <summary> + /// Saves trickplay tiles info. + /// </summary> + /// <param name="itemId">The item.</param> + /// <param name="tilesInfo">The trickplay tiles info.</param> + void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); - /// <summary> - /// Gets the trickplay manifest. - /// </summary> - /// <param name="item">The item.</param> - /// <returns>A map of media source id to a map of tile width to tile info.</returns> - Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item); + /// <summary> + /// Gets the trickplay manifest. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>A map of media source id to a map of tile width to tile info.</returns> + Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item); - /// <summary> - /// Gets the path to a trickplay tiles image. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="width">The width of a single tile.</param> - /// <param name="index">The tile grid's index.</param> - /// <returns>The absolute path.</returns> - string GetTrickplayTilePath(BaseItem item, int width, int index); - } + /// <summary> + /// Gets the path to a trickplay tiles image. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="width">The width of a single tile.</param> + /// <param name="index">The tile grid's index.</param> + /// <returns>The absolute path.</returns> + string GetTrickplayTilePath(BaseItem item, int width, int index); } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4692bf504..000831fe2 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -793,7 +793,7 @@ namespace MediaBrowser.MediaEncoding.Encoder CancellationToken cancellationToken) { var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions(); - threads = threads ?? _threads; + threads ??= _threads; // A new EncodingOptions instance must be used as to not disable HW acceleration for all of Jellyfin. // Additionally, we must set a few fields without defaults to prevent null pointer exceptions. diff --git a/MediaBrowser.Model/Configuration/TrickplayOptions.cs b/MediaBrowser.Model/Configuration/TrickplayOptions.cs index d89e5f590..1fff1a5ed 100644 --- a/MediaBrowser.Model/Configuration/TrickplayOptions.cs +++ b/MediaBrowser.Model/Configuration/TrickplayOptions.cs @@ -1,61 +1,60 @@ using System.Collections.Generic; using System.Diagnostics; -namespace MediaBrowser.Model.Configuration +namespace MediaBrowser.Model.Configuration; + +/// <summary> +/// Class TrickplayOptions. +/// </summary> +public class TrickplayOptions { /// <summary> - /// Class TrickplayOptions. - /// </summary> - public class TrickplayOptions - { - /// <summary> - /// Gets or sets a value indicating whether or not to use HW acceleration. - /// </summary> - public bool EnableHwAcceleration { get; set; } = false; - - /// <summary> - /// Gets or sets the behavior used by trickplay provider on library scan/update. - /// </summary> - public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking; - - /// <summary> - /// Gets or sets the process priority for the ffmpeg process. - /// </summary> - public ProcessPriorityClass ProcessPriority { get; set; } = ProcessPriorityClass.BelowNormal; - - /// <summary> - /// Gets or sets the interval, in ms, between each new trickplay image. - /// </summary> - public int Interval { get; set; } = 10000; - - /// <summary> - /// Gets or sets the target width resolutions, in px, to generates preview images for. - /// </summary> - public int[] WidthResolutions { get; set; } = new[] { 320 }; - - /// <summary> - /// Gets or sets number of tile images to allow in X dimension. - /// </summary> - public int TileWidth { get; set; } = 10; - - /// <summary> - /// Gets or sets number of tile images to allow in Y dimension. - /// </summary> - public int TileHeight { get; set; } = 10; - - /// <summary> - /// Gets or sets the ffmpeg output quality level. - /// </summary> - public int Qscale { get; set; } = 4; - - /// <summary> - /// Gets or sets the jpeg quality to use for image tiles. - /// </summary> - public int JpegQuality { get; set; } = 90; - - /// <summary> - /// Gets or sets the number of threads to be used by ffmpeg. - /// </summary> - public int ProcessThreads { get; set; } = 0; - } + /// Gets or sets a value indicating whether or not to use HW acceleration. + /// </summary> + public bool EnableHwAcceleration { get; set; } = false; + + /// <summary> + /// Gets or sets the behavior used by trickplay provider on library scan/update. + /// </summary> + public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking; + + /// <summary> + /// Gets or sets the process priority for the ffmpeg process. + /// </summary> + public ProcessPriorityClass ProcessPriority { get; set; } = ProcessPriorityClass.BelowNormal; + + /// <summary> + /// Gets or sets the interval, in ms, between each new trickplay image. + /// </summary> + public int Interval { get; set; } = 10000; + + /// <summary> + /// Gets or sets the target width resolutions, in px, to generates preview images for. + /// </summary> + public int[] WidthResolutions { get; set; } = new[] { 320 }; + + /// <summary> + /// Gets or sets number of tile images to allow in X dimension. + /// </summary> + public int TileWidth { get; set; } = 10; + + /// <summary> + /// Gets or sets number of tile images to allow in Y dimension. + /// </summary> + public int TileHeight { get; set; } = 10; + + /// <summary> + /// Gets or sets the ffmpeg output quality level. + /// </summary> + public int Qscale { get; set; } = 4; + + /// <summary> + /// Gets or sets the jpeg quality to use for image tiles. + /// </summary> + public int JpegQuality { get; set; } = 90; + + /// <summary> + /// Gets or sets the number of threads to be used by ffmpeg. + /// </summary> + public int ProcessThreads { get; set; } = 0; } diff --git a/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs index 799794176..d0db53218 100644 --- a/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs +++ b/MediaBrowser.Model/Configuration/TrickplayScanBehavior.cs @@ -1,18 +1,17 @@ -namespace MediaBrowser.Model.Configuration +namespace MediaBrowser.Model.Configuration; + +/// <summary> +/// Enum TrickplayScanBehavior. +/// </summary> +public enum TrickplayScanBehavior { /// <summary> - /// Enum TrickplayScanBehavior. + /// Starts generation, only return once complete. /// </summary> - public enum TrickplayScanBehavior - { - /// <summary> - /// Starts generation, only return once complete. - /// </summary> - Blocking, + Blocking, - /// <summary> - /// Start generation, return immediately. - /// </summary> - NonBlocking - } + /// <summary> + /// Start generation, return immediately. + /// </summary> + NonBlocking } diff --git a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs index 84b6b0322..86d37787f 100644 --- a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs +++ b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs @@ -1,50 +1,49 @@ -namespace MediaBrowser.Model.Entities +namespace MediaBrowser.Model.Entities; + +/// <summary> +/// Class TrickplayTilesInfo. +/// </summary> +public class TrickplayTilesInfo { /// <summary> - /// Class TrickplayTilesInfo. + /// Gets or sets width of an individual tile. /// </summary> - public class TrickplayTilesInfo - { - /// <summary> - /// Gets or sets width of an individual tile. - /// </summary> - /// <value>The width.</value> - public int Width { get; set; } + /// <value>The width.</value> + public int Width { get; set; } - /// <summary> - /// Gets or sets height of an individual tile. - /// </summary> - /// <value>The height.</value> - public int Height { get; set; } + /// <summary> + /// Gets or sets height of an individual tile. + /// </summary> + /// <value>The height.</value> + public int Height { get; set; } - /// <summary> - /// Gets or sets amount of tiles per row. - /// </summary> - /// <value>The tile grid's width.</value> - public int TileWidth { get; set; } + /// <summary> + /// Gets or sets amount of tiles per row. + /// </summary> + /// <value>The tile grid's width.</value> + public int TileWidth { get; set; } - /// <summary> - /// Gets or sets amount of tiles per column. - /// </summary> - /// <value>The tile grid's height.</value> - public int TileHeight { get; set; } + /// <summary> + /// Gets or sets amount of tiles per column. + /// </summary> + /// <value>The tile grid's height.</value> + public int TileHeight { get; set; } - /// <summary> - /// Gets or sets total amount of non-black tiles. - /// </summary> - /// <value>The tile count.</value> - public int TileCount { get; set; } + /// <summary> + /// Gets or sets total amount of non-black tiles. + /// </summary> + /// <value>The tile count.</value> + public int TileCount { get; set; } - /// <summary> - /// Gets or sets interval in milliseconds between each trickplay tile. - /// </summary> - /// <value>The interval.</value> - public int Interval { get; set; } + /// <summary> + /// Gets or sets interval in milliseconds between each trickplay tile. + /// </summary> + /// <value>The interval.</value> + public int Interval { get; set; } - /// <summary> - /// Gets or sets peak bandwith usage in bits per second. - /// </summary> - /// <value>The bandwidth.</value> - public int Bandwidth { get; set; } - } + /// <summary> + /// Gets or sets peak bandwith usage in bits per second. + /// </summary> + /// <value>The bandwidth.</value> + public int Bandwidth { get; set; } } diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs index 8d0d9d5a3..f32557cd1 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -12,98 +12,97 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Trickplay +namespace MediaBrowser.Providers.Trickplay; + +/// <summary> +/// Class TrickplayImagesTask. +/// </summary> +public class TrickplayImagesTask : IScheduledTask { + private readonly ILogger<TrickplayImagesTask> _logger; + private readonly ILibraryManager _libraryManager; + private readonly ILocalizationManager _localization; + private readonly ITrickplayManager _trickplayManager; + /// <summary> - /// Class TrickplayImagesTask. + /// Initializes a new instance of the <see cref="TrickplayImagesTask"/> class. /// </summary> - public class TrickplayImagesTask : IScheduledTask + /// <param name="logger">The logger.</param> + /// <param name="libraryManager">The library manager.</param> + /// <param name="localization">The localization manager.</param> + /// <param name="trickplayManager">The trickplay manager.</param> + public TrickplayImagesTask( + ILogger<TrickplayImagesTask> logger, + ILibraryManager libraryManager, + ILocalizationManager localization, + ITrickplayManager trickplayManager) { - private readonly ILogger<TrickplayImagesTask> _logger; - private readonly ILibraryManager _libraryManager; - private readonly ILocalizationManager _localization; - private readonly ITrickplayManager _trickplayManager; - - /// <summary> - /// Initializes a new instance of the <see cref="TrickplayImagesTask"/> class. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="libraryManager">The library manager.</param> - /// <param name="localization">The localization manager.</param> - /// <param name="trickplayManager">The trickplay manager.</param> - public TrickplayImagesTask( - ILogger<TrickplayImagesTask> logger, - ILibraryManager libraryManager, - ILocalizationManager localization, - ITrickplayManager trickplayManager) - { - _libraryManager = libraryManager; - _logger = logger; - _localization = localization; - _trickplayManager = trickplayManager; - } + _libraryManager = libraryManager; + _logger = logger; + _localization = localization; + _trickplayManager = trickplayManager; + } - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskRefreshTrickplayImages"); + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskRefreshTrickplayImages"); - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TaskRefreshTrickplayImagesDescription"); + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskRefreshTrickplayImagesDescription"); - /// <inheritdoc /> - public string Key => "RefreshTrickplayImages"; + /// <inheritdoc /> + public string Key => "RefreshTrickplayImages"; - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); - /// <inheritdoc /> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + /// <inheritdoc /> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + return new[] { - return new[] + new TaskTriggerInfo { - new TaskTriggerInfo - { - Type = TaskTriggerInfo.TriggerDaily, - TimeOfDayTicks = TimeSpan.FromHours(3).Ticks - } - }; - } + Type = TaskTriggerInfo.TriggerDaily, + TimeOfDayTicks = TimeSpan.FromHours(3).Ticks + } + }; + } - /// <inheritdoc /> - public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + /// <inheritdoc /> + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) + { + var items = _libraryManager.GetItemList(new InternalItemsQuery { - var items = _libraryManager.GetItemList(new InternalItemsQuery - { - MediaTypes = new[] { MediaType.Video }, - IsVirtualItem = false, - IsFolder = false, - Recursive = true - }).OfType<Video>().ToList(); + MediaTypes = new[] { MediaType.Video }, + IsVirtualItem = false, + IsFolder = false, + Recursive = true + }).OfType<Video>().ToList(); - var numComplete = 0; + var numComplete = 0; - foreach (var item in items) + foreach (var item in items) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + await _trickplayManager.RefreshTrickplayDataAsync(item, false, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) { - try - { - cancellationToken.ThrowIfCancellationRequested(); - await _trickplayManager.RefreshTrickplayData(item, false, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - break; - } - catch (Exception ex) - { - _logger.LogError("Error creating trickplay files for {ItemName}: {Msg}", item.Name, ex); - } + break; + } + catch (Exception ex) + { + _logger.LogError("Error creating trickplay files for {ItemName}: {Msg}", item.Name, ex); + } - numComplete++; - double percent = numComplete; - percent /= items.Count; - percent *= 100; + numComplete++; + double percent = numComplete; + percent /= items.Count; + percent *= 100; - progress.Report(percent); - } + progress.Report(percent); } } } diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs index ed2c11281..9b8eb8150 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs @@ -17,370 +17,369 @@ using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; using SkiaSharp; -namespace MediaBrowser.Providers.Trickplay +namespace MediaBrowser.Providers.Trickplay; + +/// <summary> +/// ITrickplayManager implementation. +/// </summary> +public class TrickplayManager : ITrickplayManager { + private readonly ILogger<TrickplayManager> _logger; + private readonly IItemRepository _itemRepo; + private readonly IMediaEncoder _mediaEncoder; + private readonly IFileSystem _fileSystem; + private readonly EncodingHelper _encodingHelper; + private readonly ILibraryManager _libraryManager; + private readonly IServerConfigurationManager _config; + + private static readonly SemaphoreSlim _resourcePool = new(1, 1); + /// <summary> - /// ITrickplayManager implementation. + /// Initializes a new instance of the <see cref="TrickplayManager"/> class. /// </summary> - public class TrickplayManager : ITrickplayManager + /// <param name="logger">The logger.</param> + /// <param name="itemRepo">The item repository.</param> + /// <param name="mediaEncoder">The media encoder.</param> + /// <param name="fileSystem">The file systen.</param> + /// <param name="encodingHelper">The encoding helper.</param> + /// <param name="libraryManager">The library manager.</param> + /// <param name="config">The server configuration manager.</param> + public TrickplayManager( + ILogger<TrickplayManager> logger, + IItemRepository itemRepo, + IMediaEncoder mediaEncoder, + IFileSystem fileSystem, + EncodingHelper encodingHelper, + ILibraryManager libraryManager, + IServerConfigurationManager config) { - private readonly ILogger<TrickplayManager> _logger; - private readonly IItemRepository _itemRepo; - private readonly IMediaEncoder _mediaEncoder; - private readonly IFileSystem _fileSystem; - private readonly EncodingHelper _encodingHelper; - private readonly ILibraryManager _libraryManager; - private readonly IServerConfigurationManager _config; - - private static readonly SemaphoreSlim _resourcePool = new(1, 1); - - /// <summary> - /// Initializes a new instance of the <see cref="TrickplayManager"/> class. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="itemRepo">The item repository.</param> - /// <param name="mediaEncoder">The media encoder.</param> - /// <param name="fileSystem">The file systen.</param> - /// <param name="encodingHelper">The encoding helper.</param> - /// <param name="libraryManager">The library manager.</param> - /// <param name="config">The server configuration manager.</param> - public TrickplayManager( - ILogger<TrickplayManager> logger, - IItemRepository itemRepo, - IMediaEncoder mediaEncoder, - IFileSystem fileSystem, - EncodingHelper encodingHelper, - ILibraryManager libraryManager, - IServerConfigurationManager config) + _logger = logger; + _itemRepo = itemRepo; + _mediaEncoder = mediaEncoder; + _fileSystem = fileSystem; + _encodingHelper = encodingHelper; + _libraryManager = libraryManager; + _config = config; + } + + /// <inheritdoc /> + public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken) + { + _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); + + var options = _config.Configuration.TrickplayOptions; + foreach (var width in options.WidthResolutions) { - _logger = logger; - _itemRepo = itemRepo; - _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; - _encodingHelper = encodingHelper; - _libraryManager = libraryManager; - _config = config; + cancellationToken.ThrowIfCancellationRequested(); + await RefreshTrickplayDataInternal( + video, + replace, + width, + options, + cancellationToken).ConfigureAwait(false); } + } - /// <inheritdoc /> - public async Task RefreshTrickplayData(Video video, bool replace, CancellationToken cancellationToken) + private async Task RefreshTrickplayDataInternal( + Video video, + bool replace, + int width, + TrickplayOptions options, + CancellationToken cancellationToken) + { + if (!CanGenerateTrickplay(video, options.Interval)) { - _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); - - var options = _config.Configuration.TrickplayOptions; - foreach (var width in options.WidthResolutions) - { - cancellationToken.ThrowIfCancellationRequested(); - await RefreshTrickplayDataInternal( - video, - replace, - width, - options, - cancellationToken).ConfigureAwait(false); - } + return; } - private async Task RefreshTrickplayDataInternal( - Video video, - bool replace, - int width, - TrickplayOptions options, - CancellationToken cancellationToken) + var imgTempDir = string.Empty; + var outputDir = GetTrickplayDirectory(video, width); + + try { - if (!CanGenerateTrickplay(video, options.Interval)) + await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + + if (!replace && Directory.Exists(outputDir) && GetTilesResolutions(video.Id).ContainsKey(width)) { + _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id); return; } - var imgTempDir = string.Empty; - var outputDir = GetTrickplayDirectory(video, width); + // Extract images + // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay. + var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id)); - try + if (mediaSource is null) { - await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - - if (!replace && Directory.Exists(outputDir) && GetTilesResolutions(video.Id).ContainsKey(width)) - { - _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id); - return; - } - - // Extract images - // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay. - var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id)); - - if (mediaSource is null) - { - _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id); - return; - } + _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id); + return; + } - var mediaPath = mediaSource.Path; - var mediaStream = mediaSource.VideoStream; - var container = mediaSource.Container; - - _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id); - imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated( - mediaPath, - container, - mediaSource, - mediaStream, - width, - TimeSpan.FromMilliseconds(options.Interval), - options.EnableHwAcceleration, - options.ProcessThreads, - options.Qscale, - options.ProcessPriority, - _encodingHelper, - cancellationToken).ConfigureAwait(false); - - if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir)) - { - throw new InvalidOperationException("Null or invalid directory from media encoder."); - } + var mediaPath = mediaSource.Path; + var mediaStream = mediaSource.VideoStream; + var container = mediaSource.Container; + + _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id); + imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated( + mediaPath, + container, + mediaSource, + mediaStream, + width, + TimeSpan.FromMilliseconds(options.Interval), + options.EnableHwAcceleration, + options.ProcessThreads, + options.Qscale, + options.ProcessPriority, + _encodingHelper, + cancellationToken).ConfigureAwait(false); + + if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir)) + { + throw new InvalidOperationException("Null or invalid directory from media encoder."); + } - var images = _fileSystem.GetFiles(imgTempDir, new string[] { ".jpg" }, false, false) - .Where(img => string.Equals(img.Extension, ".jpg", StringComparison.Ordinal)) - .OrderBy(i => i.FullName) - .ToList(); + var images = _fileSystem.GetFiles(imgTempDir, new string[] { ".jpg" }, false, false) + .Where(img => string.Equals(img.Extension, ".jpg", StringComparison.Ordinal)) + .OrderBy(i => i.FullName) + .ToList(); - // Create tiles - var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N")); - var tilesInfo = CreateTiles(images, width, options, tilesTempDir, outputDir); + // Create tiles + var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N")); + var tilesInfo = CreateTiles(images, width, options, tilesTempDir, outputDir); - // Save tiles info - try + // Save tiles info + try + { + if (tilesInfo is not null) { - if (tilesInfo is not null) - { - SaveTilesInfo(video.Id, tilesInfo); - _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath); - } - else - { - throw new InvalidOperationException("Null trickplay tiles info from CreateTiles."); - } + SaveTilesInfo(video.Id, tilesInfo); + _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath); } - catch (Exception ex) + else { - _logger.LogError(ex, "Error while saving trickplay tiles info."); - - // Make sure no files stay in metadata folders on failure - // if tiles info wasn't saved. - Directory.Delete(outputDir, true); + throw new InvalidOperationException("Null trickplay tiles info from CreateTiles."); } } catch (Exception ex) { - _logger.LogError(ex, "Error creating trickplay images."); - } - finally - { - _resourcePool.Release(); + _logger.LogError(ex, "Error while saving trickplay tiles info."); - if (!string.IsNullOrEmpty(imgTempDir)) - { - Directory.Delete(imgTempDir, true); - } + // Make sure no files stay in metadata folders on failure + // if tiles info wasn't saved. + Directory.Delete(outputDir, true); } } - - private TrickplayTilesInfo CreateTiles(List<FileSystemMetadata> images, int width, TrickplayOptions options, string workDir, string outputDir) + catch (Exception ex) + { + _logger.LogError(ex, "Error creating trickplay images."); + } + finally { - if (images.Count == 0) + _resourcePool.Release(); + + if (!string.IsNullOrEmpty(imgTempDir)) { - throw new InvalidOperationException("Can't create trickplay from 0 images."); + Directory.Delete(imgTempDir, true); } + } + } - Directory.CreateDirectory(workDir); + private TrickplayTilesInfo CreateTiles(List<FileSystemMetadata> images, int width, TrickplayOptions options, string workDir, string outputDir) + { + if (images.Count == 0) + { + throw new InvalidOperationException("Can't create trickplay from 0 images."); + } - var tilesInfo = new TrickplayTilesInfo - { - Width = width, - Interval = options.Interval, - TileWidth = options.TileWidth, - TileHeight = options.TileHeight, - TileCount = 0, - Bandwidth = 0 - }; - - var firstImg = SKBitmap.Decode(images[0].FullName); - if (firstImg == null) - { - throw new InvalidDataException("Could not decode image data."); - } + Directory.CreateDirectory(workDir); - tilesInfo.Height = firstImg.Height; - if (tilesInfo.Width != firstImg.Width) - { - throw new InvalidOperationException("Image width does not match config width."); - } + var tilesInfo = new TrickplayTilesInfo + { + Width = width, + Interval = options.Interval, + TileWidth = options.TileWidth, + TileHeight = options.TileHeight, + TileCount = 0, + Bandwidth = 0 + }; + + var firstImg = SKBitmap.Decode(images[0].FullName); + if (firstImg == null) + { + throw new InvalidDataException("Could not decode image data."); + } - /* - * Generate grids of trickplay image tiles - */ - var imgNo = 0; - var i = 0; - while (i < images.Count) - { - var tileGrid = new SKBitmap(tilesInfo.Width * tilesInfo.TileWidth, tilesInfo.Height * tilesInfo.TileHeight); + tilesInfo.Height = firstImg.Height; + if (tilesInfo.Width != firstImg.Width) + { + throw new InvalidOperationException("Image width does not match config width."); + } + + /* + * Generate grids of trickplay image tiles + */ + var imgNo = 0; + var i = 0; + while (i < images.Count) + { + var tileGrid = new SKBitmap(tilesInfo.Width * tilesInfo.TileWidth, tilesInfo.Height * tilesInfo.TileHeight); - using (var canvas = new SKCanvas(tileGrid)) + using (var canvas = new SKCanvas(tileGrid)) + { + for (var y = 0; y < tilesInfo.TileHeight; y++) { - for (var y = 0; y < tilesInfo.TileHeight; y++) + for (var x = 0; x < tilesInfo.TileWidth; x++) { - for (var x = 0; x < tilesInfo.TileWidth; x++) + if (i >= images.Count) { - if (i >= images.Count) - { - break; - } - - var img = SKBitmap.Decode(images[i].FullName); - if (img == null) - { - throw new InvalidDataException("Could not decode image data."); - } - - if (tilesInfo.Width != img.Width) - { - throw new InvalidOperationException("Image width does not match config width."); - } - - if (tilesInfo.Height != img.Height) - { - throw new InvalidOperationException("Image height does not match first image height."); - } - - canvas.DrawBitmap(img, x * tilesInfo.Width, y * tilesInfo.Height); - tilesInfo.TileCount++; - i++; + break; } - } - } - // Output each tile grid to singular file - var tileGridPath = Path.Combine(workDir, $"{imgNo}.jpg"); - using (var stream = File.OpenWrite(tileGridPath)) - { - tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, options.JpegQuality); - } + var img = SKBitmap.Decode(images[i].FullName); + if (img == null) + { + throw new InvalidDataException("Could not decode image data."); + } + + if (tilesInfo.Width != img.Width) + { + throw new InvalidOperationException("Image width does not match config width."); + } - var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000)); - tilesInfo.Bandwidth = Math.Max(tilesInfo.Bandwidth, bitrate); + if (tilesInfo.Height != img.Height) + { + throw new InvalidOperationException("Image height does not match first image height."); + } - imgNo++; + canvas.DrawBitmap(img, x * tilesInfo.Width, y * tilesInfo.Height); + tilesInfo.TileCount++; + i++; + } + } } - /* - * Move trickplay tiles to output directory - */ - Directory.CreateDirectory(outputDir); - - // Replace existing tile grids if they already exist - if (Directory.Exists(outputDir)) + // Output each tile grid to singular file + var tileGridPath = Path.Combine(workDir, $"{imgNo}.jpg"); + using (var stream = File.OpenWrite(tileGridPath)) { - Directory.Delete(outputDir, true); + tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, options.JpegQuality); } - MoveDirectory(workDir, outputDir); + var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000)); + tilesInfo.Bandwidth = Math.Max(tilesInfo.Bandwidth, bitrate); - return tilesInfo; + imgNo++; } - private bool CanGenerateTrickplay(Video video, int interval) - { - var videoType = video.VideoType; - if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay) - { - return false; - } - - if (video.IsPlaceHolder) - { - return false; - } + /* + * Move trickplay tiles to output directory + */ + Directory.CreateDirectory(outputDir); - if (video.IsShortcut) - { - return false; - } + // Replace existing tile grids if they already exist + if (Directory.Exists(outputDir)) + { + Directory.Delete(outputDir, true); + } - if (!video.IsCompleteMedia) - { - return false; - } + MoveDirectory(workDir, outputDir); - if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks) - { - return false; - } - - var libraryOptions = _libraryManager.GetLibraryOptions(video); - if (libraryOptions is not null) - { - if (!libraryOptions.EnableTrickplayImageExtraction) - { - return false; - } - } - else - { - return false; - } + return tilesInfo; + } - // Can't extract images if there are no video streams - return video.GetMediaStreams().Count > 0; + private bool CanGenerateTrickplay(Video video, int interval) + { + var videoType = video.VideoType; + if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay) + { + return false; } - /// <inheritdoc /> - public Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId) + if (video.IsPlaceHolder) { - return _itemRepo.GetTilesResolutions(itemId); + return false; } - /// <inheritdoc /> - public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo) + if (video.IsShortcut) { - _itemRepo.SaveTilesInfo(itemId, tilesInfo); + return false; } - /// <inheritdoc /> - public Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item) + if (!video.IsCompleteMedia) { - return _itemRepo.GetTrickplayManifest(item); + return false; } - /// <inheritdoc /> - public string GetTrickplayTilePath(BaseItem item, int width, int index) + if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks) { - return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg"); + return false; } - private string GetTrickplayDirectory(BaseItem item, int? width = null) + var libraryOptions = _libraryManager.GetLibraryOptions(video); + if (libraryOptions is not null) { - var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay"); - - return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path; + if (!libraryOptions.EnableTrickplayImageExtraction) + { + return false; + } + } + else + { + return false; } - private void MoveDirectory(string source, string destination) + // Can't extract images if there are no video streams + return video.GetMediaStreams().Count > 0; + } + + /// <inheritdoc /> + public Dictionary<int, TrickplayTilesInfo> GetTilesResolutions(Guid itemId) + { + return _itemRepo.GetTilesResolutions(itemId); + } + + /// <inheritdoc /> + public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo) + { + _itemRepo.SaveTilesInfo(itemId, tilesInfo); + } + + /// <inheritdoc /> + public Dictionary<Guid, Dictionary<int, TrickplayTilesInfo>> GetTrickplayManifest(BaseItem item) + { + return _itemRepo.GetTrickplayManifest(item); + } + + /// <inheritdoc /> + public string GetTrickplayTilePath(BaseItem item, int width, int index) + { + return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg"); + } + + private string GetTrickplayDirectory(BaseItem item, int? width = null) + { + var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay"); + + return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path; + } + + private void MoveDirectory(string source, string destination) + { + try { - try + Directory.Move(source, destination); + } + catch (IOException) + { + // Cross device move requires a copy + Directory.CreateDirectory(destination); + foreach (string file in Directory.GetFiles(source)) { - Directory.Move(source, destination); + File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true); } - catch (IOException) - { - // Cross device move requires a copy - Directory.CreateDirectory(destination); - foreach (string file in Directory.GetFiles(source)) - { - File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true); - } - Directory.Delete(source, true); - } + Directory.Delete(source, true); } } } diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs index e29646725..d467c480e 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs @@ -10,118 +10,117 @@ using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Configuration; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Trickplay +namespace MediaBrowser.Providers.Trickplay; + +/// <summary> +/// Class TrickplayProvider. Provides images and metadata for trickplay +/// scrubbing previews. +/// </summary> +public class TrickplayProvider : ICustomMetadataProvider<Episode>, + ICustomMetadataProvider<MusicVideo>, + ICustomMetadataProvider<Movie>, + ICustomMetadataProvider<Trailer>, + ICustomMetadataProvider<Video>, + IHasItemChangeMonitor, + IHasOrder, + IForcedProvider { + private readonly ILogger<TrickplayProvider> _logger; + private readonly IServerConfigurationManager _config; + private readonly ITrickplayManager _trickplayManager; + private readonly ILibraryManager _libraryManager; + /// <summary> - /// Class TrickplayProvider. Provides images and metadata for trickplay - /// scrubbing previews. + /// Initializes a new instance of the <see cref="TrickplayProvider"/> class. /// </summary> - public class TrickplayProvider : ICustomMetadataProvider<Episode>, - ICustomMetadataProvider<MusicVideo>, - ICustomMetadataProvider<Movie>, - ICustomMetadataProvider<Trailer>, - ICustomMetadataProvider<Video>, - IHasItemChangeMonitor, - IHasOrder, - IForcedProvider + /// <param name="logger">The logger.</param> + /// <param name="config">The configuration manager.</param> + /// <param name="trickplayManager">The trickplay manager.</param> + /// <param name="libraryManager">The library manager.</param> + public TrickplayProvider( + ILogger<TrickplayProvider> logger, + IServerConfigurationManager config, + ITrickplayManager trickplayManager, + ILibraryManager libraryManager) { - private readonly ILogger<TrickplayProvider> _logger; - private readonly IServerConfigurationManager _config; - private readonly ITrickplayManager _trickplayManager; - private readonly ILibraryManager _libraryManager; - - /// <summary> - /// Initializes a new instance of the <see cref="TrickplayProvider"/> class. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="config">The configuration manager.</param> - /// <param name="trickplayManager">The trickplay manager.</param> - /// <param name="libraryManager">The library manager.</param> - public TrickplayProvider( - ILogger<TrickplayProvider> logger, - IServerConfigurationManager config, - ITrickplayManager trickplayManager, - ILibraryManager libraryManager) - { - _logger = logger; - _config = config; - _trickplayManager = trickplayManager; - _libraryManager = libraryManager; - } + _logger = logger; + _config = config; + _trickplayManager = trickplayManager; + _libraryManager = libraryManager; + } - /// <inheritdoc /> - public string Name => "Trickplay Provider"; + /// <inheritdoc /> + public string Name => "Trickplay Provider"; - /// <inheritdoc /> - public int Order => 100; + /// <inheritdoc /> + public int Order => 100; - /// <inheritdoc /> - public bool HasChanged(BaseItem item, IDirectoryService directoryService) + /// <inheritdoc /> + public bool HasChanged(BaseItem item, IDirectoryService directoryService) + { + if (item.IsFileProtocol) { - if (item.IsFileProtocol) + var file = directoryService.GetFile(item.Path); + if (file is not null && item.DateModified != file.LastWriteTimeUtc) { - var file = directoryService.GetFile(item.Path); - if (file is not null && item.DateModified != file.LastWriteTimeUtc) - { - return true; - } + return true; } - - return false; } - /// <inheritdoc /> - public Task<ItemUpdateType> FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken) - { - return FetchInternal(item, options, cancellationToken); - } + return false; + } - /// <inheritdoc /> - public Task<ItemUpdateType> FetchAsync(MusicVideo item, MetadataRefreshOptions options, CancellationToken cancellationToken) - { - return FetchInternal(item, options, cancellationToken); - } + /// <inheritdoc /> + public Task<ItemUpdateType> FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + return FetchInternal(item, options, cancellationToken); + } - /// <inheritdoc /> - public Task<ItemUpdateType> FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken) - { - return FetchInternal(item, options, cancellationToken); - } + /// <inheritdoc /> + public Task<ItemUpdateType> FetchAsync(MusicVideo item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + return FetchInternal(item, options, cancellationToken); + } + + /// <inheritdoc /> + public Task<ItemUpdateType> FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + return FetchInternal(item, options, cancellationToken); + } - /// <inheritdoc /> - public Task<ItemUpdateType> FetchAsync(Trailer item, MetadataRefreshOptions options, CancellationToken cancellationToken) + /// <inheritdoc /> + public Task<ItemUpdateType> FetchAsync(Trailer item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + return FetchInternal(item, options, cancellationToken); + } + + /// <inheritdoc /> + public Task<ItemUpdateType> FetchAsync(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + return FetchInternal(item, options, cancellationToken); + } + + private async Task<ItemUpdateType> FetchInternal(Video video, MetadataRefreshOptions options, CancellationToken cancellationToken) + { + var libraryOptions = _libraryManager.GetLibraryOptions(video); + bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan; + bool replace = options.ReplaceAllImages; + + if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false)) { - return FetchInternal(item, options, cancellationToken); + return ItemUpdateType.None; } - /// <inheritdoc /> - public Task<ItemUpdateType> FetchAsync(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken) + if (_config.Configuration.TrickplayOptions.ScanBehavior == TrickplayScanBehavior.Blocking) { - return FetchInternal(item, options, cancellationToken); + await _trickplayManager.RefreshTrickplayDataAsync(video, replace, cancellationToken).ConfigureAwait(false); } - - private async Task<ItemUpdateType> FetchInternal(Video video, MetadataRefreshOptions options, CancellationToken cancellationToken) + else { - var libraryOptions = _libraryManager.GetLibraryOptions(video); - bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan; - bool replace = options.ReplaceAllImages; - - if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false)) - { - return ItemUpdateType.None; - } - - if (_config.Configuration.TrickplayOptions.ScanBehavior == TrickplayScanBehavior.Blocking) - { - await _trickplayManager.RefreshTrickplayData(video, replace, cancellationToken).ConfigureAwait(false); - } - else - { - _ = _trickplayManager.RefreshTrickplayData(video, replace, cancellationToken).ConfigureAwait(false); - } - - // The core doesn't need to trigger any save operations over this - return ItemUpdateType.None; + _ = _trickplayManager.RefreshTrickplayDataAsync(video, replace, cancellationToken).ConfigureAwait(false); } + + // The core doesn't need to trigger any save operations over this + return ItemUpdateType.None; } } |
