From ca7d1a13000ad948eebbfdeb40542312f3e37d3e Mon Sep 17 00:00:00 2001 From: nicknsy <20588554+nicknsy@users.noreply.github.com> Date: Wed, 22 Feb 2023 00:08:35 -0800 Subject: [PATCH 001/124] Trickplay generation, manager, storage --- .../ApplicationHost.cs | 3 + .../Data/SqliteItemRepository.cs | 123 ++++++ Emby.Server.Implementations/Dto/DtoService.cs | 5 + .../MediaEncoding/EncodingHelper.cs | 35 ++ .../MediaEncoding/IMediaEncoder.cs | 27 ++ .../Persistence/IItemRepository.cs | 21 + .../Trickplay/ITrickplayManager.cs | 54 +++ .../Encoder/MediaEncoder.cs | 173 +++++++++ .../Configuration/EncodingOptions.cs | 12 + MediaBrowser.Model/Dto/BaseItemDto.cs | 6 + .../Entities/TrickplayTilesInfo.cs | 50 +++ MediaBrowser.Model/Querying/ItemFields.cs | 5 + .../MediaBrowser.Providers.csproj | 1 + .../Trickplay/TrickplayImagesTask.cs | 116 ++++++ .../Trickplay/TrickplayManager.cs | 363 ++++++++++++++++++ .../Trickplay/TrickplayProvider.cs | 109 ++++++ 16 files changed, 1103 insertions(+) create mode 100644 MediaBrowser.Controller/Trickplay/ITrickplayManager.cs create mode 100644 MediaBrowser.Model/Entities/TrickplayTilesInfo.cs create mode 100644 MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs create mode 100644 MediaBrowser.Providers/Trickplay/TrickplayManager.cs create mode 100644 MediaBrowser.Providers/Trickplay/TrickplayProvider.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7969577bc0..1e0bb0cd6f 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -78,6 +78,7 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Controller.Trickplay; using MediaBrowser.Controller.TV; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; @@ -96,6 +97,7 @@ using MediaBrowser.Providers.Lyric; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.Tmdb; using MediaBrowser.Providers.Subtitles; +using MediaBrowser.Providers.Trickplay; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -591,6 +593,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index ca8f605a02..8ec24522b0 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -48,6 +48,7 @@ namespace Emby.Server.Implementations.Data { private const string FromText = " from TypedBaseItems A"; private const string ChaptersTableName = "Chapters2"; + private const string TrickplayTableName = "Trickplay"; private const string SaveItemCommandText = @"replace into TypedBaseItems @@ -383,6 +384,8 @@ namespace Emby.Server.Implementations.Data "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", + "create table if not exists " + TrickplayTableName + " (ItemId GUID, Width INT NOT NULL, Height INT NOT NULL, TileWidth INT NOT NULL, TileHeight INT NOT NULL, TileCount INT NOT NULL, Interval INT NOT NULL, Bandwidth INT NOT NULL, PRIMARY KEY (ItemId, Width))", + CreateMediaStreamsTableCommand, CreateMediaAttachmentsTableCommand, @@ -2135,6 +2138,126 @@ namespace Emby.Server.Implementations.Data } } + /// + public Dictionary GetTilesResolutions(Guid itemId) + { + CheckDisposed(); + + var tilesResolutions = new Dictionary(); + using (var connection = GetConnection(true)) + { + using (var statement = PrepareStatement(connection, "select Width,Height,TileWidth,TileHeight,TileCount,Interval,Bandwidth from " + TrickplayTableName + " where ItemId = @ItemId order by Width asc")) + { + statement.TryBind("@ItemId", itemId); + + foreach (var row in statement.ExecuteQuery()) + { + TrickplayTilesInfo tilesInfo = GetTrickplayTilesInfo(row); + tilesResolutions[tilesInfo.Width] = tilesInfo; + } + } + } + + return tilesResolutions; + } + + /// + public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo) + { + CheckDisposed(); + + ArgumentNullException.ThrowIfNull(tilesInfo); + + var idBlob = itemId.ToByteArray(); + using (var connection = GetConnection(false)) + { + connection.RunInTransaction( + db => + { + // Delete old tiles info + db.Execute("delete from " + TrickplayTableName + " where ItemId=@ItemId and Width=@Width", idBlob, tilesInfo.Width); + db.Execute( + "insert into " + TrickplayTableName + " values (@ItemId, @Width, @Height, @TileWidth, @TileHeight, @TileCount, @Interval, @Bandwidth)", + idBlob, + tilesInfo.Width, + tilesInfo.Height, + tilesInfo.TileWidth, + tilesInfo.TileHeight, + tilesInfo.TileCount, + tilesInfo.Interval, + tilesInfo.Bandwidth); + }, + TransactionMode); + } + } + + /// + public Dictionary> GetTrickplayManifest(BaseItem item) + { + CheckDisposed(); + + var trickplayManifest = new Dictionary>(); + foreach (var mediaSource in item.GetMediaSources(false)) + { + var mediaSourceId = Guid.Parse(mediaSource.Id); + var tilesResolutions = GetTilesResolutions(mediaSourceId); + + if (tilesResolutions.Count > 0) + { + trickplayManifest[mediaSourceId] = tilesResolutions; + } + } + + return trickplayManifest; + } + + /// + /// Gets the trickplay tiles info. + /// + /// The reader. + /// TrickplayTilesInfo. + private TrickplayTilesInfo GetTrickplayTilesInfo(IReadOnlyList reader) + { + var tilesInfo = new TrickplayTilesInfo(); + + if (reader.TryGetInt32(0, out var width)) + { + tilesInfo.Width = width; + } + + if (reader.TryGetInt32(1, out var height)) + { + tilesInfo.Height = height; + } + + if (reader.TryGetInt32(2, out var tileWidth)) + { + tilesInfo.TileWidth = tileWidth; + } + + if (reader.TryGetInt32(3, out var tileHeight)) + { + tilesInfo.TileHeight = tileHeight; + } + + if (reader.TryGetInt32(4, out var tileCount)) + { + tilesInfo.TileCount = tileCount; + } + + if (reader.TryGetInt32(5, out var interval)) + { + tilesInfo.Interval = interval; + } + + if (reader.TryGetInt32(6, out var bandwidth)) + { + tilesInfo.Bandwidth = bandwidth; + } + + return tilesInfo; + } + private static bool EnableJoinUserData(InternalItemsQuery query) { if (query.User is null) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 7a6ed2cb80..10352b6ff8 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1058,6 +1058,11 @@ namespace Emby.Server.Implementations.Dto dto.Chapters = _itemRepo.GetChapters(item); } + if (options.ContainsField(ItemFields.Trickplay)) + { + dto.Trickplay = _itemRepo.GetTrickplayManifest(item); + } + if (video.ExtraType.HasValue) { dto.ExtraType = video.ExtraType.Value.ToString(); diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index b155d674de..0889a90f48 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -149,6 +149,36 @@ namespace MediaBrowser.Controller.MediaEncoding return defaultEncoder; } + private string GetMjpegEncoder(EncodingJobInfo state, EncodingOptions encodingOptions) + { + var defaultEncoder = "mjpeg"; + + if (state.VideoType == VideoType.VideoFile) + { + var hwType = encodingOptions.HardwareAccelerationType; + + var codecMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "vaapi", defaultEncoder + "_vaapi" }, + { "qsv", defaultEncoder + "_qsv" } + }; + + if (!string.IsNullOrEmpty(hwType) + && encodingOptions.EnableHardwareEncoding + && codecMap.ContainsKey(hwType)) + { + var preferredEncoder = codecMap[hwType]; + + if (_mediaEncoder.SupportsEncoder(preferredEncoder)) + { + return preferredEncoder; + } + } + } + + return defaultEncoder; + } + private bool IsVaapiSupported(EncodingJobInfo state) { // vaapi will throw an error with this input @@ -277,6 +307,11 @@ namespace MediaBrowser.Controller.MediaEncoding return GetH264Encoder(state, encodingOptions); } + if (string.Equals(codec, "mjpeg", StringComparison.OrdinalIgnoreCase)) + { + return GetMjpegEncoder(state, encodingOptions); + } + if (string.Equals(codec, "vp8", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index f830b9f298..aa9faa9369 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; @@ -137,6 +138,32 @@ namespace MediaBrowser.Controller.MediaEncoding /// Location of video image. Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, ImageFormat? targetFormat, CancellationToken cancellationToken); + /// + /// Extracts the video images on interval. + /// + /// Input file. + /// Video container type. + /// Media source information. + /// Media stream information. + /// The interval. + /// The maximum width. + /// Allow for hardware acceleration. + /// Allow for hardware encoding. allowHwAccel must also be true. + /// EncodingHelper instance. + /// The cancellation token. + /// Directory where images where extracted. A given image made before another will always be named with a lower number. + Task ExtractVideoImagesOnIntervalAccelerated( + string inputFile, + string container, + MediaSourceInfo mediaSource, + MediaStream imageStream, + TimeSpan interval, + int maxWidth, + bool allowHwAccel, + bool allowHwEncode, + EncodingHelper encodingHelper, + CancellationToken cancellationToken); + /// /// Gets the media info. /// diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 2c52b2b45e..11eb4932c9 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -61,6 +61,27 @@ namespace MediaBrowser.Controller.Persistence /// The list of chapters to save. void SaveChapters(Guid id, IReadOnlyList chapters); + /// + /// Get available trickplay resolutions and corresponding info. + /// + /// The item. + /// Map of width resolutions to trickplay tiles info. + Dictionary GetTilesResolutions(Guid itemId); + + /// + /// Saves trickplay tiles info. + /// + /// The item. + /// The trickplay tiles info. + void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); + + /// + /// Gets trickplay data for an item. + /// + /// The item. + /// A map of media source id to a map of tile width to tile info. + Dictionary> GetTrickplayManifest(BaseItem item); + /// /// Gets the media streams. /// diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs new file mode 100644 index 0000000000..bae458f98c --- /dev/null +++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Trickplay +{ + /// + /// Interface ITrickplayManager. + /// + public interface ITrickplayManager + { + /// + /// Generate or replace trickplay data. + /// + /// The video. + /// Whether or not existing data should be replaced. + /// CancellationToken to use for operation. + /// Task. + Task RefreshTrickplayData(Video video, bool replace, CancellationToken cancellationToken); + + /// + /// Get available trickplay resolutions and corresponding info. + /// + /// The item. + /// Map of width resolutions to trickplay tiles info. + Dictionary GetTilesResolutions(Guid itemId); + + /// + /// Saves trickplay tiles info. + /// + /// The item. + /// The trickplay tiles info. + void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo); + + /// + /// Gets the trickplay manifest. + /// + /// The item. + /// A map of media source id to a map of tile width to tile info. + Dictionary> GetTrickplayManifest(BaseItem item); + + /// + /// Gets the path to a trickplay tiles image. + /// + /// The item. + /// The width of a single tile. + /// The tile grid's index. + /// The absolute path. + string GetTrickplayTilePath(BaseItem item, int width, int index); + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4e63d205c9..7f8ec03fac 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -21,6 +21,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Probing; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; @@ -28,8 +29,10 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; +using Microsoft.AspNetCore.Components.Forms; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using static Nikse.SubtitleEdit.Core.Common.IfoParser; namespace MediaBrowser.MediaEncoding.Encoder { @@ -775,6 +778,176 @@ namespace MediaBrowser.MediaEncoding.Encoder } /// + public Task ExtractVideoImagesOnIntervalAccelerated( + string inputFile, + string container, + MediaSourceInfo mediaSource, + MediaStream imageStream, + TimeSpan interval, + int maxWidth, + bool allowHwAccel, + bool allowHwEncode, + EncodingHelper encodingHelper, + CancellationToken cancellationToken) + { + var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions(); + + // 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. + if (!allowHwAccel) + { + options.EnableHardwareEncoding = false; + options.HardwareAccelerationType = string.Empty; + options.EnableTonemapping = false; + } + + var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth }; + var jobState = new EncodingJobInfo(TranscodingJobType.Progressive) + { + IsVideoRequest = true, // must be true for InputVideoHwaccelArgs to return non-empty value + MediaSource = mediaSource, + VideoStream = imageStream, + BaseRequest = baseRequest, // GetVideoProcessingFilterParam errors if null + MediaPath = inputFile, + OutputVideoCodec = "mjpeg" + }; + var vidEncoder = options.AllowMjpegEncoding ? encodingHelper.GetVideoEncoder(jobState, options) : jobState.OutputVideoCodec; + + // Get input and filter arguments + var inputArg = encodingHelper.GetInputArgument(jobState, options, container).Trim(); + if (string.IsNullOrWhiteSpace(inputArg)) + { + throw new InvalidOperationException("EncodingHelper returned empty input arguments."); + } + + if (!allowHwAccel) + { + inputArg = "-threads " + _threads + " " + inputArg; // HW accel args set a different input thread count, only set if disabled + } + + var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, jobState.OutputVideoCodec).Trim(); + if (string.IsNullOrWhiteSpace(filterParam) || filterParam.IndexOf("\"", StringComparison.Ordinal) == -1) + { + throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters."); + } + + return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, interval, vidEncoder, _threads, cancellationToken); + } + + private async Task ExtractVideoImagesOnIntervalInternal( + string inputArg, + string filterParam, + TimeSpan interval, + string vidEncoder, + int outputThreads, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(inputArg)) + { + throw new InvalidOperationException("Empty or invalid input argument."); + } + + // Output arguments + string fps = "fps=1/" + interval.TotalSeconds.ToString(CultureInfo.InvariantCulture); + if (string.IsNullOrWhiteSpace(filterParam)) + { + filterParam = "-vf \"" + fps + "\""; + } + else + { + filterParam = filterParam.Insert(filterParam.IndexOf("\"", StringComparison.Ordinal) + 1, fps + ","); + } + + var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(targetDirectory); + var outputPath = Path.Combine(targetDirectory, "%08d.jpg"); + + // Final command arguments + var args = string.Format( + CultureInfo.InvariantCulture, + "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} -f {4} \"{5}\"", + inputArg, + filterParam, + outputThreads, + vidEncoder, + "image2", + outputPath); + + // Start ffmpeg process + var process = new Process + { + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + FileName = _ffmpegPath, + Arguments = args, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false, + }, + EnableRaisingEvents = true + }; + + var processDescription = string.Format(CultureInfo.InvariantCulture, "{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); + _logger.LogDebug("{ProcessDescription}", processDescription); + + using (var processWrapper = new ProcessWrapper(process, this)) + { + bool ranToCompletion = false; + + await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + StartProcess(processWrapper); + + // Need to give ffmpeg enough time to make all the thumbnails, which could be a while, + // but we still need to detect if the process hangs. + // Making the assumption that as long as new jpegs are showing up, everything is good. + + bool isResponsive = true; + int lastCount = 0; + + while (isResponsive) + { + if (await process.WaitForExitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false)) + { + ranToCompletion = true; + break; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var jpegCount = _fileSystem.GetFilePaths(targetDirectory) + .Count(i => string.Equals(Path.GetExtension(i), ".jpg", StringComparison.OrdinalIgnoreCase)); + + isResponsive = jpegCount > lastCount; + lastCount = jpegCount; + } + + if (!ranToCompletion) + { + _logger.LogInformation("Killing ffmpeg extraction process due to inactivity."); + StopProcess(processWrapper, 1000); + } + } + finally + { + _thumbnailResourcePool.Release(); + } + + var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1; + + if (exitCode == -1) + { + _logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription); + + throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", processDescription)); + } + + return targetDirectory; + } + } + public string GetTimeParameter(long ticks) { var time = TimeSpan.FromTicks(ticks); diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index a53be0fee7..e1d9e00b7a 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -48,7 +48,9 @@ public class EncodingOptions EnableIntelLowPowerH264HwEncoder = false; EnableIntelLowPowerHevcHwEncoder = false; EnableHardwareEncoding = true; + EnableTrickplayHwAccel = false; AllowHevcEncoding = false; + AllowMjpegEncoding = false; EnableSubtitleExtraction = true; AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" }; HardwareDecodingCodecs = new string[] { "h264", "vc1" }; @@ -244,11 +246,21 @@ public class EncodingOptions /// public bool EnableHardwareEncoding { get; set; } + /// + /// Gets or sets a value indicating whether hardware acceleration is enabled for trickplay generation. + /// + public bool EnableTrickplayHwAccel { get; set; } + /// /// Gets or sets a value indicating whether HEVC encoding is enabled. /// public bool AllowHevcEncoding { get; set; } + /// + /// Gets or sets a value indicating whether MJPEG encoding is enabled. + /// + public bool AllowMjpegEncoding { get; set; } + /// /// Gets or sets a value indicating whether subtitle extraction is enabled. /// diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 8fab1ca6d6..ab424c6f5c 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -568,6 +568,12 @@ namespace MediaBrowser.Model.Dto /// The chapters. public List Chapters { get; set; } + /// + /// Gets or sets the trickplay manifest. + /// + /// The trickplay manifest. + public Dictionary> Trickplay { get; set; } + /// /// Gets or sets the type of the location. /// diff --git a/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs new file mode 100644 index 0000000000..84b6b03228 --- /dev/null +++ b/MediaBrowser.Model/Entities/TrickplayTilesInfo.cs @@ -0,0 +1,50 @@ +namespace MediaBrowser.Model.Entities +{ + /// + /// Class TrickplayTilesInfo. + /// + public class TrickplayTilesInfo + { + /// + /// Gets or sets width of an individual tile. + /// + /// The width. + public int Width { get; set; } + + /// + /// Gets or sets height of an individual tile. + /// + /// The height. + public int Height { get; set; } + + /// + /// Gets or sets amount of tiles per row. + /// + /// The tile grid's width. + public int TileWidth { get; set; } + + /// + /// Gets or sets amount of tiles per column. + /// + /// The tile grid's height. + public int TileHeight { get; set; } + + /// + /// Gets or sets total amount of non-black tiles. + /// + /// The tile count. + public int TileCount { get; set; } + + /// + /// Gets or sets interval in milliseconds between each trickplay tile. + /// + /// The interval. + public int Interval { get; set; } + + /// + /// Gets or sets peak bandwith usage in bits per second. + /// + /// The bandwidth. + public int Bandwidth { get; set; } + } +} diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index 6fa1d778ad..242a1c6e99 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -34,6 +34,11 @@ namespace MediaBrowser.Model.Querying /// Chapters, + /// + /// The trickplay manifest. + /// + Trickplay, + ChildCount, /// diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 6a40833d7f..c836c8ed53 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -22,6 +22,7 @@ + diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs new file mode 100644 index 0000000000..3d1450a906 --- /dev/null +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Trickplay; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace MediaBrowser.Providers.Trickplay +{ + /// + /// Class TrickplayImagesTask. + /// + public class TrickplayImagesTask : IScheduledTask + { + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + private readonly ILocalizationManager _localization; + private readonly IServerConfigurationManager _configurationManager; + private readonly ITrickplayManager _trickplayManager; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The library manager. + /// The localization manager. + /// The configuration manager. + /// The trickplay manager. + public TrickplayImagesTask( + ILogger logger, + ILibraryManager libraryManager, + ILocalizationManager localization, + IServerConfigurationManager configurationManager, + ITrickplayManager trickplayManager) + { + _libraryManager = libraryManager; + _logger = logger; + _localization = localization; + _configurationManager = configurationManager; + _trickplayManager = trickplayManager; + } + + /// + public string Name => _localization.GetLocalizedString("TaskRefreshTrickplayImages"); + + /// + public string Description => _localization.GetLocalizedString("TaskRefreshTrickplayImagesDescription"); + + /// + public string Key => "RefreshTrickplayImages"; + + /// + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); + + /// + public IEnumerable GetDefaultTriggers() + { + return new[] + { + new TaskTriggerInfo + { + Type = TaskTriggerInfo.TriggerDaily, + TimeOfDayTicks = TimeSpan.FromHours(3).Ticks, + MaxRuntimeTicks = TimeSpan.FromHours(5).Ticks + } + }; + } + + /// + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + { + // TODO: libraryoptions dont run on libraries with trickplay disabled + var items = _libraryManager.GetItemList(new InternalItemsQuery + { + MediaTypes = new[] { MediaType.Video }, + IsVirtualItem = false, + IsFolder = false, + Recursive = false + }).OfType