From 8bb44b85d7008ee167f2260934a1f325abb06e2d Mon Sep 17 00:00:00 2001
From: herby2212 <12448284+herby2212@users.noreply.github.com>
Date: Mon, 1 May 2023 16:24:15 +0200
Subject: close inactive sessions after 10 minutes
---
MediaBrowser.Controller/Session/SessionInfo.cs | 6 ++++++
1 file changed, 6 insertions(+)
(limited to 'MediaBrowser.Controller')
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 25bf23d61..172d79a59 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/MediaBrowser.Controller/Session/SessionInfo.cs
@@ -109,6 +109,12 @@ namespace MediaBrowser.Controller.Session
/// The last playback check in.
public DateTime LastPlaybackCheckIn { get; set; }
+ ///
+ /// Gets or sets the last paused date.
+ ///
+ /// The last paused date.
+ public DateTime? LastPausedDate { get; set; }
+
///
/// Gets or sets the name of the device.
///
--
cgit v1.2.3
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: Trickplay generation, manager, storage
---
Emby.Server.Implementations/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 +++
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 173 ++++++++++
.../Configuration/EncodingOptions.cs | 12 +
MediaBrowser.Model/Dto/BaseItemDto.cs | 6 +
MediaBrowser.Model/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
(limited to 'MediaBrowser.Controller')
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 7969577bc..1e0bb0cd6 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 ca8f605a0..8ec24522b 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 7a6ed2cb8..10352b6ff 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 b155d674d..0889a90f4 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 f830b9f29..aa9faa936 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 2c52b2b45..11eb4932c 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 000000000..bae458f98
--- /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 4e63d205c..7f8ec03fa 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 a53be0fee..e1d9e00b7 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 8fab1ca6d..ab424c6f5 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 000000000..84b6b0322
--- /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 6fa1d778a..242a1c6e9 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 6a40833d7..c836c8ed5 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 000000000..3d1450a90
--- /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
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.
///
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index df6829946..7718f822b 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -36,6 +36,10 @@ namespace MediaBrowser.Model.Configuration
public bool ExtractChapterImagesDuringLibraryScan { get; set; }
+ public bool EnableTrickplayImageExtraction { get; set; }
+
+ public bool ExtractTrickplayImagesDuringLibraryScan { get; set; }
+
public MediaPathInfo[] PathInfos { get; set; }
public bool SaveLocalMetadata { get; set; }
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
index 62180804f..4b4514897 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Trickplay;
@@ -26,6 +27,7 @@ namespace MediaBrowser.Providers.Trickplay
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
private readonly EncodingHelper _encodingHelper;
+ private readonly ILibraryManager _libraryManager;
private static readonly SemaphoreSlim _resourcePool = new(1, 1);
@@ -37,18 +39,21 @@ namespace MediaBrowser.Providers.Trickplay
/// The media encoder.
/// The file systen.
/// The encoding helper.
+ /// The library manager.
public TrickplayManager(
ILogger logger,
IItemRepository itemRepo,
IMediaEncoder mediaEncoder,
IFileSystem fileSystem,
- EncodingHelper encodingHelper)
+ EncodingHelper encodingHelper,
+ ILibraryManager libraryManager)
{
_logger = logger;
_itemRepo = itemRepo;
_mediaEncoder = mediaEncoder;
_fileSystem = fileSystem;
_encodingHelper = encodingHelper;
+ _libraryManager = libraryManager;
}
///
@@ -287,11 +292,15 @@ namespace MediaBrowser.Providers.Trickplay
return false;
}
- /* TODO config options
+ 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.EnableChapterImageExtraction)
+ if (!libraryOptions.EnableTrickplayImageExtraction)
{
return false;
}
@@ -300,12 +309,6 @@ namespace MediaBrowser.Providers.Trickplay
{
return false;
}
- */
-
- if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks)
- {
- return false;
- }
// Can't extract images if there are no video streams
return video.GetMediaStreams().Count > 0;
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
index be66dea8a..2b3879ca3 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
@@ -27,6 +27,7 @@ namespace MediaBrowser.Providers.Trickplay
private readonly ILogger _logger;
private readonly IServerConfigurationManager _configurationManager;
private readonly ITrickplayManager _trickplayManager;
+ private readonly ILibraryManager _libraryManager;
///
/// Initializes a new instance of the class.
@@ -34,21 +35,24 @@ namespace MediaBrowser.Providers.Trickplay
/// The logger.
/// The configuration manager.
/// The trickplay manager.
+ /// The library manager.
public TrickplayProvider(
ILogger logger,
IServerConfigurationManager configurationManager,
- ITrickplayManager trickplayManager)
+ ITrickplayManager trickplayManager,
+ ILibraryManager libraryManager)
{
_logger = logger;
_configurationManager = configurationManager;
_trickplayManager = trickplayManager;
+ _libraryManager = libraryManager;
}
///
- public string Name => "Trickplay Preview";
+ public string Name => "Trickplay Provider";
///
- public int Order => 1000;
+ public int Order => 100;
///
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
@@ -95,11 +99,24 @@ namespace MediaBrowser.Providers.Trickplay
return FetchInternal(item, options, cancellationToken);
}
- private async Task FetchInternal(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ private async Task FetchInternal(Video video, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
- // TODO: implement all config options -->
+ var libraryOptions = _libraryManager.GetLibraryOptions(video);
+ bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan;
+ bool replace = options.ReplaceAllImages;
+
+ if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false))
+ {
+ _logger.LogDebug("exit refresh: automated - {0} enable scan - {1}", options.IsAutomated, enableDuringScan.GetValueOrDefault(false));
+ return ItemUpdateType.None;
+ }
+
// TODO: this is always blocking for metadata collection, make non-blocking option
- await _trickplayManager.RefreshTrickplayData(item, options.ReplaceAllImages, cancellationToken).ConfigureAwait(false);
+ if (true)
+ {
+ _logger.LogDebug("called refresh");
+ await _trickplayManager.RefreshTrickplayData(video, replace, cancellationToken).ConfigureAwait(false);
+ }
// The core doesn't need to trigger any save operations over this
return ItemUpdateType.None;
--
cgit v1.2.3
From 16ea7baad4030074e291159f3b35ebb009872544 Mon Sep 17 00:00:00 2001
From: nicknsy <20588554+nicknsy@users.noreply.github.com>
Date: Thu, 23 Feb 2023 16:10:18 -0800
Subject: Stay consistent with patch branch
---
MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs | 1 -
1 file changed, 1 deletion(-)
(limited to 'MediaBrowser.Controller')
diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
index 004c16ba2..9e91a8bcd 100644
--- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
+++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
@@ -25,7 +25,6 @@ namespace MediaBrowser.Controller.Providers
ForceSave = copy.ForceSave;
ReplaceAllMetadata = copy.ReplaceAllMetadata;
EnableRemoteContentProbe = copy.EnableRemoteContentProbe;
- IsAutomated = copy.IsAutomated;
IsAutomated = copy.IsAutomated;
ImageRefreshMode = copy.ImageRefreshMode;
--
cgit v1.2.3
From 6744e712d3a4fd6f800e5499c90b247787e48cb6 Mon Sep 17 00:00:00 2001
From: nicknsy <20588554+nicknsy@users.noreply.github.com>
Date: Sat, 25 Feb 2023 15:59:46 -0800
Subject: Use config values
---
.../MediaEncoding/IMediaEncoder.cs | 13 ++++--
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 37 +++++++++++-----
.../Configuration/ServerConfiguration.cs | 2 +
.../Trickplay/TrickplayImagesTask.cs | 12 ++++--
.../Trickplay/TrickplayManager.cs | 49 +++++++++++++++-------
.../Trickplay/TrickplayProvider.cs | 16 ++++---
6 files changed, 89 insertions(+), 40 deletions(-)
(limited to 'MediaBrowser.Controller')
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index aa9faa936..f5e3d03cb 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Configuration;
@@ -145,10 +146,12 @@ namespace MediaBrowser.Controller.MediaEncoding
/// Video container type.
/// Media source information.
/// Media stream information.
- /// The interval.
/// The maximum width.
+ /// The interval.
/// Allow for hardware acceleration.
- /// Allow for hardware encoding. allowHwAccel must also be true.
+ /// The input/output thread count for ffmpeg.
+ /// The qscale value for ffmpeg.
+ /// The process priority for the ffmpeg process.
/// EncodingHelper instance.
/// The cancellation token.
/// Directory where images where extracted. A given image made before another will always be named with a lower number.
@@ -157,10 +160,12 @@ namespace MediaBrowser.Controller.MediaEncoding
string container,
MediaSourceInfo mediaSource,
MediaStream imageStream,
- TimeSpan interval,
int maxWidth,
+ TimeSpan interval,
bool allowHwAccel,
- bool allowHwEncode,
+ int? threads,
+ int? qualityScale,
+ ProcessPriorityClass? priority,
EncodingHelper encodingHelper,
CancellationToken cancellationToken);
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 9b58f83b4..11f42c3f9 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -783,14 +783,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
string container,
MediaSourceInfo mediaSource,
MediaStream imageStream,
- TimeSpan interval,
int maxWidth,
+ TimeSpan interval,
bool allowHwAccel,
- bool allowHwEncode,
+ int? threads,
+ int? qualityScale,
+ ProcessPriorityClass? priority,
EncodingHelper encodingHelper,
CancellationToken cancellationToken)
{
var options = allowHwAccel ? _configurationManager.GetEncodingOptions() : new EncodingOptions();
+ 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.
@@ -822,7 +825,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (!allowHwAccel)
{
- inputArg = "-threads " + _threads + " " + inputArg; // HW accel args set a different input thread count, only set if disabled
+ 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();
@@ -831,7 +834,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
}
- return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, interval, vidEncoder, _threads, cancellationToken);
+ return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, interval, vidEncoder, threads, qualityScale, priority, cancellationToken);
}
private async Task ExtractVideoImagesOnIntervalInternal(
@@ -839,7 +842,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
string filterParam,
TimeSpan interval,
string vidEncoder,
- int outputThreads,
+ int? outputThreads,
+ int? qualityScale,
+ ProcessPriorityClass? priority,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(inputArg))
@@ -857,10 +862,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
filterParam = filterParam.Insert(filterParam.IndexOf("\"", StringComparison.Ordinal) + 1, fps + ",");
}
- else
- {
- filterParam += fps + ",";
- }
var targetDirectory = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(targetDirectory);
@@ -869,11 +870,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Final command arguments
var args = string.Format(
CultureInfo.InvariantCulture,
- "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} -f {4} \"{5}\"",
+ "-loglevel error {0} -an -sn {1} -threads {2} -c:v {3} {4}-f {5} \"{6}\"",
inputArg,
filterParam,
- outputThreads,
+ outputThreads.GetValueOrDefault(_threads),
vidEncoder,
+ qualityScale.HasValue ? "-qscale:v " + qualityScale.Value.ToString(CultureInfo.InvariantCulture) + " " : string.Empty,
"image2",
outputPath);
@@ -904,6 +906,19 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
StartProcess(processWrapper);
+ // Set process priority
+ if (priority.HasValue)
+ {
+ try
+ {
+ processWrapper.Process.PriorityClass = priority.Value;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogDebug(ex, "Unable to set process priority to {Priority} for {Description}", priority.Value, processDescription);
+ }
+ }
+
// 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.
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 78a310f0b..097eff295 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -264,5 +264,7 @@ namespace MediaBrowser.Model.Configuration
///
/// The limit for parallel image encoding.
public int ParallelImageEncodingLimit { get; set; }
+
+ public TrickplayOptions TrickplayOptions { get; set; } = new TrickplayOptions();
}
}
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
index 87ac145d7..a364926c0 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
@@ -22,7 +22,6 @@ namespace MediaBrowser.Providers.Trickplay
private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization;
- private readonly IServerConfigurationManager _configurationManager;
private readonly ITrickplayManager _trickplayManager;
///
@@ -31,19 +30,16 @@ namespace MediaBrowser.Providers.Trickplay
/// 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;
}
@@ -77,6 +73,14 @@ namespace MediaBrowser.Providers.Trickplay
public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken)
{
// TODO: libraryoptions dont run on libraries with trickplay disabled
+ /* will this still get all sub-items? should recursive be true?
+ * from chapterimagestask
+ * DtoOptions = new DtoOptions(false)
+ {
+ EnableImages = false
+ },
+ SourceTypes = new SourceType[] { SourceType.Library },
+ */
var items = _libraryManager.GetItemList(new InternalItemsQuery
{
MediaTypes = new[] { MediaType.Video },
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
index cb916dfdb..ed2c11281 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
@@ -5,11 +5,13 @@ using System.IO;
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.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
@@ -28,6 +30,7 @@ namespace MediaBrowser.Providers.Trickplay
private readonly IFileSystem _fileSystem;
private readonly EncodingHelper _encodingHelper;
private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _config;
private static readonly SemaphoreSlim _resourcePool = new(1, 1);
@@ -40,13 +43,15 @@ namespace MediaBrowser.Providers.Trickplay
/// The file systen.
/// The encoding helper.
/// The library manager.
+ /// The server configuration manager.
public TrickplayManager(
ILogger logger,
IItemRepository itemRepo,
IMediaEncoder mediaEncoder,
IFileSystem fileSystem,
EncodingHelper encodingHelper,
- ILibraryManager libraryManager)
+ ILibraryManager libraryManager,
+ IServerConfigurationManager config)
{
_logger = logger;
_itemRepo = itemRepo;
@@ -54,6 +59,7 @@ namespace MediaBrowser.Providers.Trickplay
_fileSystem = fileSystem;
_encodingHelper = encodingHelper;
_libraryManager = libraryManager;
+ _config = config;
}
///
@@ -61,16 +67,27 @@ namespace MediaBrowser.Providers.Trickplay
{
_logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
- foreach (var width in new int[] { 320 } /*todo conf*/)
+ var options = _config.Configuration.TrickplayOptions;
+ foreach (var width in options.WidthResolutions)
{
cancellationToken.ThrowIfCancellationRequested();
- await RefreshTrickplayData(video, replace, width, 10000/*todo conf*/, 10/*todo conf*/, 10/*todo conf*/, true/*todo conf*/, true/*todo conf*/, cancellationToken).ConfigureAwait(false);
+ await RefreshTrickplayDataInternal(
+ video,
+ replace,
+ width,
+ options,
+ cancellationToken).ConfigureAwait(false);
}
}
- private async Task RefreshTrickplayData(Video video, bool replace, int width, int interval, int tileWidth, int tileHeight, bool doHwAccel, bool doHwEncode, CancellationToken cancellationToken)
+ private async Task RefreshTrickplayDataInternal(
+ Video video,
+ bool replace,
+ int width,
+ TrickplayOptions options,
+ CancellationToken cancellationToken)
{
- if (!CanGenerateTrickplay(video, interval))
+ if (!CanGenerateTrickplay(video, options.Interval))
{
return;
}
@@ -108,10 +125,12 @@ namespace MediaBrowser.Providers.Trickplay
container,
mediaSource,
mediaStream,
- TimeSpan.FromMilliseconds(interval),
width,
- doHwAccel,
- doHwEncode,
+ TimeSpan.FromMilliseconds(options.Interval),
+ options.EnableHwAcceleration,
+ options.ProcessThreads,
+ options.Qscale,
+ options.ProcessPriority,
_encodingHelper,
cancellationToken).ConfigureAwait(false);
@@ -127,7 +146,7 @@ namespace MediaBrowser.Providers.Trickplay
// Create tiles
var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N"));
- var tilesInfo = CreateTiles(images, width, interval, tileWidth, tileHeight, 100/* todo _config.JpegQuality*/, tilesTempDir, outputDir);
+ var tilesInfo = CreateTiles(images, width, options, tilesTempDir, outputDir);
// Save tiles info
try
@@ -166,7 +185,7 @@ namespace MediaBrowser.Providers.Trickplay
}
}
- private TrickplayTilesInfo CreateTiles(List images, int width, int interval, int tileWidth, int tileHeight, int quality, string workDir, string outputDir)
+ private TrickplayTilesInfo CreateTiles(List images, int width, TrickplayOptions options, string workDir, string outputDir)
{
if (images.Count == 0)
{
@@ -178,9 +197,9 @@ namespace MediaBrowser.Providers.Trickplay
var tilesInfo = new TrickplayTilesInfo
{
Width = width,
- Interval = interval,
- TileWidth = tileWidth,
- TileHeight = tileHeight,
+ Interval = options.Interval,
+ TileWidth = options.TileWidth,
+ TileHeight = options.TileHeight,
TileCount = 0,
Bandwidth = 0
};
@@ -244,7 +263,7 @@ namespace MediaBrowser.Providers.Trickplay
var tileGridPath = Path.Combine(workDir, $"{imgNo}.jpg");
using (var stream = File.OpenWrite(tileGridPath))
{
- tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, quality);
+ tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, options.JpegQuality);
}
var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tileGridPath).Length * 8 / tilesInfo.TileWidth / tilesInfo.TileHeight / (tilesInfo.Interval / 1000));
@@ -351,7 +370,7 @@ namespace MediaBrowser.Providers.Trickplay
{
Directory.Move(source, destination);
}
- catch (System.IO.IOException)
+ catch (IOException)
{
// Cross device move requires a copy
Directory.CreateDirectory(destination);
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
index e4bd9e3c2..e29646725 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs
@@ -7,6 +7,7 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Configuration;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Trickplay
@@ -25,7 +26,7 @@ namespace MediaBrowser.Providers.Trickplay
IForcedProvider
{
private readonly ILogger _logger;
- private readonly IServerConfigurationManager _configurationManager;
+ private readonly IServerConfigurationManager _config;
private readonly ITrickplayManager _trickplayManager;
private readonly ILibraryManager _libraryManager;
@@ -33,17 +34,17 @@ namespace MediaBrowser.Providers.Trickplay
/// Initializes a new instance of the class.
///
/// The logger.
- /// The configuration manager.
+ /// The configuration manager.
/// The trickplay manager.
/// The library manager.
public TrickplayProvider(
ILogger logger,
- IServerConfigurationManager configurationManager,
+ IServerConfigurationManager config,
ITrickplayManager trickplayManager,
ILibraryManager libraryManager)
{
_logger = logger;
- _configurationManager = configurationManager;
+ _config = config;
_trickplayManager = trickplayManager;
_libraryManager = libraryManager;
}
@@ -110,11 +111,14 @@ namespace MediaBrowser.Providers.Trickplay
return ItemUpdateType.None;
}
- // TODO: this is always blocking for metadata collection, make non-blocking option
- if (true)
+ 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;
--
cgit v1.2.3
From dd8ef08592830236b31307e2424b491e974f024a Mon Sep 17 00:00:00 2001
From: Nick <20588554+nicknsy@users.noreply.github.com>
Date: Wed, 29 Mar 2023 16:43:17 -0700
Subject: Move fps filter to GetVideoProcessingFilterParam
---
.../MediaEncoding/EncodingHelper.cs | 9 +++++++++
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 19 ++++---------------
2 files changed, 13 insertions(+), 15 deletions(-)
(limited to 'MediaBrowser.Controller')
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 0889a90f4..bcdf2934a 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -4806,6 +4806,15 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters?.RemoveAll(filter => string.IsNullOrEmpty(filter));
overlayFilters?.RemoveAll(filter => string.IsNullOrEmpty(filter));
+ var framerate = GetFramerateParam(state);
+ if (framerate.HasValue)
+ {
+ mainFilters.Insert(0, string.Format(
+ CultureInfo.InvariantCulture,
+ "fps={0}",
+ framerate.Value));
+ }
+
var mainStr = string.Empty;
if (mainFilters?.Count > 0)
{
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 11f42c3f9..4692bf504 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -804,7 +804,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
options.EnableTonemapping = false;
}
- var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth };
+ var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.TotalSeconds) };
var jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
{
IsVideoRequest = true, // must be true for InputVideoHwaccelArgs to return non-empty value
@@ -829,18 +829,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
var filterParam = encodingHelper.GetVideoProcessingFilterParam(jobState, options, jobState.OutputVideoCodec).Trim();
- if (string.IsNullOrWhiteSpace(filterParam) || filterParam.IndexOf("\"", StringComparison.Ordinal) == -1)
+ if (string.IsNullOrWhiteSpace(filterParam))
{
throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
}
- return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, interval, vidEncoder, threads, qualityScale, priority, cancellationToken);
+ return ExtractVideoImagesOnIntervalInternal(inputArg, filterParam, vidEncoder, threads, qualityScale, priority, cancellationToken);
}
private async Task ExtractVideoImagesOnIntervalInternal(
string inputArg,
string filterParam,
- TimeSpan interval,
string vidEncoder,
int? outputThreads,
int? qualityScale,
@@ -853,16 +852,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
}
// Output arguments
- string fps = "fps=1/" + interval.TotalSeconds.ToString(CultureInfo.InvariantCulture);
- if (string.IsNullOrWhiteSpace(filterParam))
- {
- filterParam = "-vf \"" + fps + "\"";
- }
- else if (filterParam.IndexOf("\"", StringComparison.Ordinal) != -1)
- {
- 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");
@@ -895,7 +884,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
};
var processDescription = string.Format(CultureInfo.InvariantCulture, "{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
- _logger.LogDebug("{ProcessDescription}", processDescription);
+ _logger.LogInformation("Trickplay generation: {ProcessDescription}", processDescription);
using (var processWrapper = new ProcessWrapper(process, this))
{
--
cgit v1.2.3
From 33770322282326304b4b8073f583d6fed2354c0b Mon Sep 17 00:00:00 2001
From: Nick <20588554+nicknsy@users.noreply.github.com>
Date: Mon, 1 May 2023 12:51:05 -0700
Subject: crobibero styling, format, code suggestions
---
.../MediaEncoding/EncodingHelper.cs | 27 +-
.../Trickplay/ITrickplayManager.cs | 77 ++-
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 2 +-
.../Configuration/TrickplayOptions.cs | 109 ++--
.../Configuration/TrickplayScanBehavior.cs | 25 +-
MediaBrowser.Model/Entities/TrickplayTilesInfo.cs | 79 ++-
.../Trickplay/TrickplayImagesTask.cs | 147 +++---
.../Trickplay/TrickplayManager.cs | 569 ++++++++++-----------
.../Trickplay/TrickplayProvider.cs | 181 ++++---
9 files changed, 602 insertions(+), 614 deletions(-)
(limited to 'MediaBrowser.Controller')
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 _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(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;
+
+///
+/// Interface ITrickplayManager.
+///
+public interface ITrickplayManager
{
///
- /// Interface ITrickplayManager.
+ /// Generate or replace trickplay data.
///
- 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);
+ /// The video.
+ /// Whether or not existing data should be replaced.
+ /// CancellationToken to use for operation.
+ /// Task.
+ Task RefreshTrickplayDataAsync(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);
+ ///
+ /// 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);
+ ///
+ /// 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 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);
- }
+ ///
+ /// 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 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;
+
+///
+/// Class TrickplayOptions.
+///
+public class TrickplayOptions
{
///
- /// Class TrickplayOptions.
- ///
- public class TrickplayOptions
- {
- ///
- /// Gets or sets a value indicating whether or not to use HW acceleration.
- ///
- public bool EnableHwAcceleration { get; set; } = false;
-
- ///
- /// Gets or sets the behavior used by trickplay provider on library scan/update.
- ///
- public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking;
-
- ///
- /// Gets or sets the process priority for the ffmpeg process.
- ///
- public ProcessPriorityClass ProcessPriority { get; set; } = ProcessPriorityClass.BelowNormal;
-
- ///
- /// Gets or sets the interval, in ms, between each new trickplay image.
- ///
- public int Interval { get; set; } = 10000;
-
- ///
- /// Gets or sets the target width resolutions, in px, to generates preview images for.
- ///
- public int[] WidthResolutions { get; set; } = new[] { 320 };
-
- ///
- /// Gets or sets number of tile images to allow in X dimension.
- ///
- public int TileWidth { get; set; } = 10;
-
- ///
- /// Gets or sets number of tile images to allow in Y dimension.
- ///
- public int TileHeight { get; set; } = 10;
-
- ///
- /// Gets or sets the ffmpeg output quality level.
- ///
- public int Qscale { get; set; } = 4;
-
- ///
- /// Gets or sets the jpeg quality to use for image tiles.
- ///
- public int JpegQuality { get; set; } = 90;
-
- ///
- /// Gets or sets the number of threads to be used by ffmpeg.
- ///
- public int ProcessThreads { get; set; } = 0;
- }
+ /// Gets or sets a value indicating whether or not to use HW acceleration.
+ ///
+ public bool EnableHwAcceleration { get; set; } = false;
+
+ ///
+ /// Gets or sets the behavior used by trickplay provider on library scan/update.
+ ///
+ public TrickplayScanBehavior ScanBehavior { get; set; } = TrickplayScanBehavior.NonBlocking;
+
+ ///
+ /// Gets or sets the process priority for the ffmpeg process.
+ ///
+ public ProcessPriorityClass ProcessPriority { get; set; } = ProcessPriorityClass.BelowNormal;
+
+ ///
+ /// Gets or sets the interval, in ms, between each new trickplay image.
+ ///
+ public int Interval { get; set; } = 10000;
+
+ ///
+ /// Gets or sets the target width resolutions, in px, to generates preview images for.
+ ///
+ public int[] WidthResolutions { get; set; } = new[] { 320 };
+
+ ///
+ /// Gets or sets number of tile images to allow in X dimension.
+ ///
+ public int TileWidth { get; set; } = 10;
+
+ ///
+ /// Gets or sets number of tile images to allow in Y dimension.
+ ///
+ public int TileHeight { get; set; } = 10;
+
+ ///
+ /// Gets or sets the ffmpeg output quality level.
+ ///
+ public int Qscale { get; set; } = 4;
+
+ ///
+ /// Gets or sets the jpeg quality to use for image tiles.
+ ///
+ public int JpegQuality { get; set; } = 90;
+
+ ///
+ /// Gets or sets the number of threads to be used by ffmpeg.
+ ///
+ 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;
+
+///
+/// Enum TrickplayScanBehavior.
+///
+public enum TrickplayScanBehavior
{
///
- /// Enum TrickplayScanBehavior.
+ /// Starts generation, only return once complete.
///
- public enum TrickplayScanBehavior
- {
- ///
- /// Starts generation, only return once complete.
- ///
- Blocking,
+ Blocking,
- ///
- /// Start generation, return immediately.
- ///
- NonBlocking
- }
+ ///
+ /// Start generation, return immediately.
+ ///
+ 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;
+
+///
+/// Class TrickplayTilesInfo.
+///
+public class TrickplayTilesInfo
{
///
- /// Class TrickplayTilesInfo.
+ /// Gets or sets width of an individual tile.
///
- public class TrickplayTilesInfo
- {
- ///
- /// Gets or sets width of an individual tile.
- ///
- /// The width.
- public int Width { get; set; }
+ /// 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 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 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 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 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 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; }
- }
+ ///
+ /// Gets or sets peak bandwith usage in bits per second.
+ ///
+ /// The bandwidth.
+ 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;
+
+///
+/// Class TrickplayImagesTask.
+///
+public class TrickplayImagesTask : IScheduledTask
{
+ private readonly ILogger _logger;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILocalizationManager _localization;
+ private readonly ITrickplayManager _trickplayManager;
+
///
- /// Class TrickplayImagesTask.
+ /// Initializes a new instance of the class.
///
- public class TrickplayImagesTask : IScheduledTask
+ /// The logger.
+ /// The library manager.
+ /// The localization manager.
+ /// The trickplay manager.
+ public TrickplayImagesTask(
+ ILogger logger,
+ ILibraryManager libraryManager,
+ ILocalizationManager localization,
+ ITrickplayManager trickplayManager)
{
- private readonly ILogger _logger;
- private readonly ILibraryManager _libraryManager;
- private readonly ILocalizationManager _localization;
- private readonly ITrickplayManager _trickplayManager;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- /// The library manager.
- /// The localization manager.
- /// The trickplay manager.
- public TrickplayImagesTask(
- ILogger logger,
- ILibraryManager libraryManager,
- ILocalizationManager localization,
- ITrickplayManager trickplayManager)
- {
- _libraryManager = libraryManager;
- _logger = logger;
- _localization = localization;
- _trickplayManager = trickplayManager;
- }
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _localization = localization;
+ _trickplayManager = trickplayManager;
+ }
- ///
- public string Name => _localization.GetLocalizedString("TaskRefreshTrickplayImages");
+ ///
+ public string Name => _localization.GetLocalizedString("TaskRefreshTrickplayImages");
- ///
- public string Description => _localization.GetLocalizedString("TaskRefreshTrickplayImagesDescription");
+ ///
+ public string Description => _localization.GetLocalizedString("TaskRefreshTrickplayImagesDescription");
- ///
- public string Key => "RefreshTrickplayImages";
+ ///
+ public string Key => "RefreshTrickplayImages";
- ///
- public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
+ ///
+ public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
- ///
- public IEnumerable GetDefaultTriggers()
+ ///
+ public IEnumerable 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
+ }
+ };
+ }
- ///
- public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken)
+ ///
+ public async Task ExecuteAsync(IProgress 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().ToList();
+ MediaTypes = new[] { MediaType.Video },
+ IsVirtualItem = false,
+ IsFolder = false,
+ Recursive = true
+ }).OfType().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;
+
+///
+/// ITrickplayManager implementation.
+///
+public class TrickplayManager : ITrickplayManager
{
+ private readonly ILogger _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);
+
///
- /// ITrickplayManager implementation.
+ /// Initializes a new instance of the class.
///
- public class TrickplayManager : ITrickplayManager
+ /// The logger.
+ /// The item repository.
+ /// The media encoder.
+ /// The file systen.
+ /// The encoding helper.
+ /// The library manager.
+ /// The server configuration manager.
+ public TrickplayManager(
+ ILogger logger,
+ IItemRepository itemRepo,
+ IMediaEncoder mediaEncoder,
+ IFileSystem fileSystem,
+ EncodingHelper encodingHelper,
+ ILibraryManager libraryManager,
+ IServerConfigurationManager config)
{
- private readonly ILogger _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);
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- /// The item repository.
- /// The media encoder.
- /// The file systen.
- /// The encoding helper.
- /// The library manager.
- /// The server configuration manager.
- public TrickplayManager(
- ILogger 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;
+ }
+
+ ///
+ 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);
}
+ }
- ///
- 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 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 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;
}
- ///
- public Dictionary GetTilesResolutions(Guid itemId)
+ if (video.IsPlaceHolder)
{
- return _itemRepo.GetTilesResolutions(itemId);
+ return false;
}
- ///
- public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo)
+ if (video.IsShortcut)
{
- _itemRepo.SaveTilesInfo(itemId, tilesInfo);
+ return false;
}
- ///
- public Dictionary> GetTrickplayManifest(BaseItem item)
+ if (!video.IsCompleteMedia)
{
- return _itemRepo.GetTrickplayManifest(item);
+ return false;
}
- ///
- 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;
+ }
+
+ ///
+ public Dictionary GetTilesResolutions(Guid itemId)
+ {
+ return _itemRepo.GetTilesResolutions(itemId);
+ }
+
+ ///
+ public void SaveTilesInfo(Guid itemId, TrickplayTilesInfo tilesInfo)
+ {
+ _itemRepo.SaveTilesInfo(itemId, tilesInfo);
+ }
+
+ ///
+ public Dictionary> GetTrickplayManifest(BaseItem item)
+ {
+ return _itemRepo.GetTrickplayManifest(item);
+ }
+
+ ///
+ 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;
+
+///
+/// Class TrickplayProvider. Provides images and metadata for trickplay
+/// scrubbing previews.
+///
+public class TrickplayProvider : ICustomMetadataProvider,
+ ICustomMetadataProvider,
+ ICustomMetadataProvider,
+ ICustomMetadataProvider,
+ ICustomMetadataProvider,
+ IHasItemChangeMonitor,
+ IHasOrder,
+ IForcedProvider
{
+ private readonly ILogger _logger;
+ private readonly IServerConfigurationManager _config;
+ private readonly ITrickplayManager _trickplayManager;
+ private readonly ILibraryManager _libraryManager;
+
///
- /// Class TrickplayProvider. Provides images and metadata for trickplay
- /// scrubbing previews.
+ /// Initializes a new instance of the class.
///
- public class TrickplayProvider : ICustomMetadataProvider,
- ICustomMetadataProvider,
- ICustomMetadataProvider,
- ICustomMetadataProvider,
- ICustomMetadataProvider,
- IHasItemChangeMonitor,
- IHasOrder,
- IForcedProvider
+ /// The logger.
+ /// The configuration manager.
+ /// The trickplay manager.
+ /// The library manager.
+ public TrickplayProvider(
+ ILogger logger,
+ IServerConfigurationManager config,
+ ITrickplayManager trickplayManager,
+ ILibraryManager libraryManager)
{
- private readonly ILogger _logger;
- private readonly IServerConfigurationManager _config;
- private readonly ITrickplayManager _trickplayManager;
- private readonly ILibraryManager _libraryManager;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- /// The configuration manager.
- /// The trickplay manager.
- /// The library manager.
- public TrickplayProvider(
- ILogger logger,
- IServerConfigurationManager config,
- ITrickplayManager trickplayManager,
- ILibraryManager libraryManager)
- {
- _logger = logger;
- _config = config;
- _trickplayManager = trickplayManager;
- _libraryManager = libraryManager;
- }
+ _logger = logger;
+ _config = config;
+ _trickplayManager = trickplayManager;
+ _libraryManager = libraryManager;
+ }
- ///
- public string Name => "Trickplay Provider";
+ ///
+ public string Name => "Trickplay Provider";
- ///
- public int Order => 100;
+ ///
+ public int Order => 100;
- ///
- public bool HasChanged(BaseItem item, IDirectoryService directoryService)
+ ///
+ 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;
}
- ///
- public Task FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken)
- {
- return FetchInternal(item, options, cancellationToken);
- }
+ return false;
+ }
- ///
- public Task FetchAsync(MusicVideo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
- {
- return FetchInternal(item, options, cancellationToken);
- }
+ ///
+ public Task FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
- ///
- public Task FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken)
- {
- return FetchInternal(item, options, cancellationToken);
- }
+ ///
+ public Task FetchAsync(MusicVideo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
+
+ ///
+ public Task FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
- ///
- public Task FetchAsync(Trailer item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ ///
+ public Task FetchAsync(Trailer item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
+
+ ///
+ public Task FetchAsync(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken)
+ {
+ return FetchInternal(item, options, cancellationToken);
+ }
+
+ private async Task 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;
}
- ///
- public Task 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 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;
}
}
--
cgit v1.2.3
From 0e2c362078c5b0babaa0fd254106452e6d67ebe8 Mon Sep 17 00:00:00 2001
From: Nick <20588554+nicknsy@users.noreply.github.com>
Date: Tue, 30 May 2023 14:23:02 -0700
Subject: Move SkiaSharp related code to Jellyfin.Drawing and IImageEncoder
---
MediaBrowser.Controller/Drawing/IImageEncoder.cs | 11 +++
.../MediaBrowser.Providers.csproj | 3 +-
.../Trickplay/TrickplayManager.cs | 93 ++++++++--------------
src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 79 ++++++++++++++++++
src/Jellyfin.Drawing/NullImageEncoder.cs | 6 ++
5 files changed, 129 insertions(+), 63 deletions(-)
(limited to 'MediaBrowser.Controller')
diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
index e5c8ebfaf..42c680761 100644
--- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs
+++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Drawing;
namespace MediaBrowser.Controller.Drawing
@@ -81,5 +82,15 @@ namespace MediaBrowser.Controller.Drawing
/// The list of poster paths.
/// The list of backdrop paths.
void CreateSplashscreen(IReadOnlyList posters, IReadOnlyList backdrops);
+
+ ///
+ /// Creates a new jpeg trickplay grid image.
+ ///
+ /// The options to use when creating the image. Width and Height are a quantity of tiles in this case, not pixels.
+ /// The image encode quality.
+ /// The width of a single trickplay image.
+ /// Optional height of a single trickplay image, if it is known.
+ /// Height of single decoded trickplay image.
+ int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight);
}
}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index c836c8ed5..7ef70f4b0 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -1,4 +1,4 @@
-
+
@@ -22,7 +22,6 @@
-
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
index 2304f803e..419adc4b0 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -15,7 +16,6 @@ using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
-using SkiaSharp;
namespace MediaBrowser.Providers.Trickplay;
@@ -31,6 +31,7 @@ public class TrickplayManager : ITrickplayManager
private readonly EncodingHelper _encodingHelper;
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _config;
+ private readonly IImageEncoder _imageEncoder;
private static readonly SemaphoreSlim _resourcePool = new(1, 1);
private static readonly string[] _trickplayImgExtensions = { ".jpg" };
@@ -45,6 +46,7 @@ public class TrickplayManager : ITrickplayManager
/// The encoding helper.
/// The library manager.
/// The server configuration manager.
+ /// The image encoder.
public TrickplayManager(
ILogger logger,
IItemRepository itemRepo,
@@ -52,7 +54,8 @@ public class TrickplayManager : ITrickplayManager
IFileSystem fileSystem,
EncodingHelper encodingHelper,
ILibraryManager libraryManager,
- IServerConfigurationManager config)
+ IServerConfigurationManager config,
+ IImageEncoder imageEncoder)
{
_logger = logger;
_itemRepo = itemRepo;
@@ -61,6 +64,7 @@ public class TrickplayManager : ITrickplayManager
_encodingHelper = encodingHelper;
_libraryManager = libraryManager;
_config = config;
+ _imageEncoder = imageEncoder;
}
///
@@ -141,7 +145,8 @@ public class TrickplayManager : ITrickplayManager
}
var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false)
- .OrderBy(i => i.FullName)
+ .Select(i => i.FullName)
+ .OrderBy(i => i)
.ToList();
// Create tiles
@@ -185,11 +190,11 @@ public class TrickplayManager : ITrickplayManager
}
}
- private TrickplayTilesInfo CreateTiles(List images, int width, TrickplayOptions options, string workDir, string outputDir)
+ private TrickplayTilesInfo CreateTiles(List images, int width, TrickplayOptions options, string workDir, string outputDir)
{
if (images.Count == 0)
{
- throw new InvalidOperationException("Can't create trickplay from 0 images.");
+ throw new ArgumentException("Can't create trickplay from 0 images.");
}
Directory.CreateDirectory(workDir);
@@ -200,76 +205,42 @@ public class TrickplayManager : ITrickplayManager
Interval = options.Interval,
TileWidth = options.TileWidth,
TileHeight = options.TileHeight,
- TileCount = 0,
+ TileCount = images.Count,
+ // Set during image generation
+ Height = 0,
Bandwidth = 0
};
- var firstImg = SKBitmap.Decode(images[0].FullName);
- if (firstImg == null)
+ /*
+ * Generate trickplay tile grids from sets of images
+ */
+ var imageOptions = new ImageCollageOptions
{
- throw new InvalidDataException("Could not decode image data.");
- }
+ Width = tilesInfo.TileWidth,
+ Height = tilesInfo.TileHeight
+ };
- tilesInfo.Height = firstImg.Height;
- if (tilesInfo.Width != firstImg.Width)
- {
- throw new InvalidOperationException("Image width does not match config width.");
- }
+ var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight;
+ var requiredTileGrids = (int)Math.Ceiling((double)images.Count / tilesPerGrid);
- /*
- * Generate grids of trickplay image tiles
- */
- var imgNo = 0;
- var i = 0;
- while (i < images.Count)
+ for (int i = 0; i < requiredTileGrids; i++)
{
- var tileGrid = new SKBitmap(tilesInfo.Width * tilesInfo.TileWidth, tilesInfo.Height * tilesInfo.TileHeight);
+ // Set output/input paths
+ var tileGridPath = Path.Combine(workDir, $"{i}.jpg");
- using (var canvas = new SKCanvas(tileGrid))
- {
- for (var y = 0; y < tilesInfo.TileHeight; y++)
- {
- for (var x = 0; x < tilesInfo.TileWidth; x++)
- {
- 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++;
- }
- }
- }
+ imageOptions.OutputPath = tileGridPath;
+ imageOptions.InputPaths = images.Skip(i * tilesPerGrid).Take(tilesPerGrid).ToList();
- // Output each tile grid to singular file
- var tileGridPath = Path.Combine(workDir, $"{imgNo}.jpg");
- using (var stream = File.OpenWrite(tileGridPath))
+ // Generate image and use returned height for tiles info
+ var height = _imageEncoder.CreateTrickplayGrid(imageOptions, options.JpegQuality, tilesInfo.Width, tilesInfo.Height != 0 ? tilesInfo.Height : null);
+ if (tilesInfo.Height == 0)
{
- tileGrid.Encode(stream, SKEncodedImageFormat.Jpeg, options.JpegQuality);
+ tilesInfo.Height = height;
}
+ // Update bitrate
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);
-
- imgNo++;
}
/*
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 2d980db18..2facf0f37 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -2,14 +2,18 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
+using System.Linq;
+using System.Security.Cryptography.Xml;
using BlurHashSharp.SkiaSharp;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Drawing;
using Microsoft.Extensions.Logging;
using SkiaSharp;
+using static System.Net.Mime.MediaTypeNames;
using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
namespace Jellyfin.Drawing.Skia;
@@ -526,6 +530,81 @@ public class SkiaEncoder : IImageEncoder
splashBuilder.GenerateSplash(posters, backdrops, outputPath);
}
+ ///
+ public int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight)
+ {
+ var paths = options.InputPaths;
+ var tileWidth = options.Width;
+ var tileHeight = options.Height;
+
+ if (paths.Count < 1)
+ {
+ throw new ArgumentException("InputPaths cannot be empty.");
+ }
+ else if (paths.Count > tileWidth * tileHeight)
+ {
+ throw new ArgumentException($"InputPaths contains more images than would fit on {tileWidth}x{tileHeight} grid.");
+ }
+
+ // If no height provided, use height of first image.
+ if (!imgHeight.HasValue)
+ {
+ using var firstImg = Decode(paths[0], false, null, out _);
+
+ if (firstImg is null)
+ {
+ throw new InvalidDataException("Could not decode image data.");
+ }
+
+ if (firstImg.Width != imgWidth)
+ {
+ throw new InvalidOperationException("Image width does not match provided width.");
+ }
+
+ imgHeight = firstImg.Height;
+ }
+
+ // Make horizontal strips using every provided image.
+ using var tileGrid = new SKBitmap(imgWidth * tileWidth, imgHeight.Value * tileHeight);
+ using var canvas = new SKCanvas(tileGrid);
+
+ var imgIndex = 0;
+ for (var y = 0; y < tileHeight; y++)
+ {
+ for (var x = 0; x < tileWidth; x++)
+ {
+ if (imgIndex >= paths.Count)
+ {
+ break;
+ }
+
+ using var img = Decode(paths[imgIndex++], false, null, out _);
+
+ if (img is null)
+ {
+ throw new InvalidDataException("Could not decode image data.");
+ }
+
+ if (img.Width != imgWidth)
+ {
+ throw new InvalidOperationException("Image width does not match provided width.");
+ }
+
+ if (img.Height != imgHeight)
+ {
+ throw new InvalidOperationException("Image height does not match first image height.");
+ }
+
+ canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value);
+ }
+ }
+
+ using var outputStream = new SKFileWStream(options.OutputPath);
+ tileGrid.Encode(outputStream, SKEncodedImageFormat.Jpeg, quality);
+
+ return imgHeight.Value;
+ }
+
private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
{
try
diff --git a/src/Jellyfin.Drawing/NullImageEncoder.cs b/src/Jellyfin.Drawing/NullImageEncoder.cs
index 171128bed..15345e1bc 100644
--- a/src/Jellyfin.Drawing/NullImageEncoder.cs
+++ b/src/Jellyfin.Drawing/NullImageEncoder.cs
@@ -49,6 +49,12 @@ public class NullImageEncoder : IImageEncoder
throw new NotImplementedException();
}
+ ///
+ public int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight)
+ {
+ throw new NotImplementedException();
+ }
+
///
public string GetImageBlurHash(int xComp, int yComp, string path)
{
--
cgit v1.2.3
From 619d1d47f27e3ca2f2f249fa81fe23f8019ec0e7 Mon Sep 17 00:00:00 2001
From: Nick <20588554+nicknsy@users.noreply.github.com>
Date: Fri, 23 Jun 2023 14:22:00 -0700
Subject: Move GetHlsPlaylist to ITrickplayManager
---
Jellyfin.Api/Controllers/TrickplayController.cs | 86 +++-------------------
.../Trickplay/ITrickplayManager.cs | 9 +++
.../Trickplay/TrickplayManager.cs | 76 +++++++++++++++++++
3 files changed, 94 insertions(+), 77 deletions(-)
(limited to 'MediaBrowser.Controller')
diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs
index ac71eff19..36464d726 100644
--- a/Jellyfin.Api/Controllers/TrickplayController.cs
+++ b/Jellyfin.Api/Controllers/TrickplayController.cs
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
-using System.Globalization;
using System.Net.Mime;
using System.Text;
using Jellyfin.Api.Attributes;
@@ -54,7 +53,14 @@ public class TrickplayController : BaseJellyfinApiController
[FromRoute, Required] int width,
[FromQuery] Guid? mediaSourceId)
{
- return GetTrickplayPlaylistInternal(width, mediaSourceId ?? itemId);
+ string? playlist = _trickplayManager.GetHlsPlaylist(mediaSourceId ?? itemId, width, User.GetToken());
+
+ if (string.IsNullOrEmpty(playlist))
+ {
+ return NotFound();
+ }
+
+ return new FileContentResult(Encoding.UTF8.GetBytes(playlist), MimeTypes.GetMimeType("playlist.m3u8"));
}
///
@@ -71,7 +77,7 @@ public class TrickplayController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
- public ActionResult GetTrickplayHlsPlaylist(
+ public ActionResult GetTrickplayGridImage(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] int width,
[FromRoute, Required] int index,
@@ -91,78 +97,4 @@ public class TrickplayController : BaseJellyfinApiController
return NotFound();
}
-
- private ActionResult GetTrickplayPlaylistInternal(int width, Guid mediaSourceId)
- {
- var tilesResolutions = _trickplayManager.GetTilesResolutions(mediaSourceId);
- if (tilesResolutions is not null && tilesResolutions.TryGetValue(width, out var tilesInfo))
- {
- var builder = new StringBuilder(128);
-
- if (tilesInfo.TileCount > 0)
- {
- const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}";
- const string decimalFormat = "{0:0.###}";
-
- var resolution = $"{tilesInfo.Width}x{tilesInfo.Height}";
- var layout = $"{tilesInfo.TileWidth}x{tilesInfo.TileHeight}";
- var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight;
- var tileDuration = tilesInfo.Interval / 1000d;
- var infDuration = tileDuration * tilesPerGrid;
- var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid);
-
- builder
- .AppendLine("#EXTM3U")
- .Append("#EXT-X-TARGETDURATION:")
- .AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture))
- .AppendLine("#EXT-X-VERSION:7")
- .AppendLine("#EXT-X-MEDIA-SEQUENCE:1")
- .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
- .AppendLine("#EXT-X-IMAGES-ONLY");
-
- for (int i = 0; i < tileGridCount; i++)
- {
- // All tile grids before the last one must contain full amount of tiles.
- // The final grid will be 0 < count <= maxTiles
- if (i == tileGridCount - 1)
- {
- tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid);
- infDuration = tileDuration * tilesPerGrid;
- }
-
- // EXTINF
- builder
- .Append("#EXTINF:")
- .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, infDuration)
- .AppendLine(",");
-
- // EXT-X-TILES
- builder
- .Append("#EXT-X-TILES:RESOLUTION=")
- .Append(resolution)
- .Append(",LAYOUT=")
- .Append(layout)
- .Append(",DURATION=")
- .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, tileDuration)
- .AppendLine();
-
- // URL
- builder
- .AppendFormat(
- CultureInfo.InvariantCulture,
- urlFormat,
- width.ToString(CultureInfo.InvariantCulture),
- i.ToString(CultureInfo.InvariantCulture),
- mediaSourceId.ToString("N"),
- User.GetToken())
- .AppendLine();
- }
-
- builder.AppendLine("#EXT-X-ENDLIST");
- return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
- }
- }
-
- return NotFound();
- }
}
diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
index 8e82c57d4..8d36fc3ff 100644
--- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
+++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
@@ -50,4 +50,13 @@ public interface ITrickplayManager
/// The tile grid's index.
/// The absolute path.
string GetTrickplayTilePath(BaseItem item, int width, int index);
+
+ ///
+ /// Gets the trickplay HLS playlist.
+ ///
+ /// The item.
+ /// The width of a single tile.
+ /// Optional api key of the requesting user.
+ /// The text content of the .m3u8 playlist.
+ string? GetHlsPlaylist(Guid itemId, int width, string? apiKey);
}
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
index 419adc4b0..9fe3a330a 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayManager.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
@@ -321,6 +322,81 @@ public class TrickplayManager : ITrickplayManager
return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg");
}
+ ///
+ public string? GetHlsPlaylist(Guid itemId, int width, string? apiKey)
+ {
+ var tilesResolutions = GetTilesResolutions(itemId);
+ if (tilesResolutions is not null && tilesResolutions.TryGetValue(width, out var tilesInfo))
+ {
+ var builder = new StringBuilder(128);
+
+ if (tilesInfo.TileCount > 0)
+ {
+ const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}";
+ const string decimalFormat = "{0:0.###}";
+
+ var resolution = $"{tilesInfo.Width}x{tilesInfo.Height}";
+ var layout = $"{tilesInfo.TileWidth}x{tilesInfo.TileHeight}";
+ var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight;
+ var tileDuration = tilesInfo.Interval / 1000d;
+ var infDuration = tileDuration * tilesPerGrid;
+ var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid);
+
+ builder
+ .AppendLine("#EXTM3U")
+ .Append("#EXT-X-TARGETDURATION:")
+ .AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture))
+ .AppendLine("#EXT-X-VERSION:7")
+ .AppendLine("#EXT-X-MEDIA-SEQUENCE:1")
+ .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
+ .AppendLine("#EXT-X-IMAGES-ONLY");
+
+ for (int i = 0; i < tileGridCount; i++)
+ {
+ // All tile grids before the last one must contain full amount of tiles.
+ // The final grid will be 0 < count <= maxTiles
+ if (i == tileGridCount - 1)
+ {
+ tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid);
+ infDuration = tileDuration * tilesPerGrid;
+ }
+
+ // EXTINF
+ builder
+ .Append("#EXTINF:")
+ .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, infDuration)
+ .AppendLine(",");
+
+ // EXT-X-TILES
+ builder
+ .Append("#EXT-X-TILES:RESOLUTION=")
+ .Append(resolution)
+ .Append(",LAYOUT=")
+ .Append(layout)
+ .Append(",DURATION=")
+ .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, tileDuration)
+ .AppendLine();
+
+ // URL
+ builder
+ .AppendFormat(
+ CultureInfo.InvariantCulture,
+ urlFormat,
+ width.ToString(CultureInfo.InvariantCulture),
+ i.ToString(CultureInfo.InvariantCulture),
+ itemId.ToString("N"),
+ apiKey)
+ .AppendLine();
+ }
+
+ builder.AppendLine("#EXT-X-ENDLIST");
+ return builder.ToString();
+ }
+ }
+
+ return null;
+ }
+
private string GetTrickplayDirectory(BaseItem item, int? width = null)
{
var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay");
--
cgit v1.2.3
From ab20ceaad65b2e72fe6e823aa6086e2c6ac36844 Mon Sep 17 00:00:00 2001
From: Nick <20588554+nicknsy@users.noreply.github.com>
Date: Mon, 26 Jun 2023 17:40:10 -0700
Subject: Migrate to trickplay table to EF. Rename vars/methods/members to have
consistent use of tile and thumbnail
---
Emby.Server.Implementations/ApplicationHost.cs | 3 -
.../Data/SqliteItemRepository.cs | 124 ----
Emby.Server.Implementations/Dto/DtoService.cs | 10 +-
Jellyfin.Api/Controllers/TrickplayController.cs | 19 +-
Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 18 +-
Jellyfin.Data/Entities/TrickplayInfo.cs | 75 +++
.../JellyfinDbContext.cs | 5 +
.../20230626233818_AddTrickplayInfos.Designer.cs | 681 +++++++++++++++++++++
.../Migrations/20230626233818_AddTrickplayInfos.cs | 40 ++
.../Migrations/JellyfinDbModelSnapshot.cs | 35 +-
.../TrickplayInfoConfiguration.cs | 18 +
.../Trickplay/TrickplayManager.cs | 468 ++++++++++++++
Jellyfin.Server/CoreAppHost.cs | 3 +
MediaBrowser.Controller/Drawing/IImageEncoder.cs | 13 +-
.../Persistence/IItemRepository.cs | 21 -
.../Trickplay/ITrickplayManager.cs | 30 +-
MediaBrowser.Model/Dto/BaseItemDto.cs | 3 +-
MediaBrowser.Model/Entities/TrickplayTilesInfo.cs | 49 --
.../Trickplay/TrickplayManager.cs | 425 -------------
src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 2 +-
src/Jellyfin.Drawing/NullImageEncoder.cs | 2 +-
21 files changed, 1375 insertions(+), 669 deletions(-)
create mode 100644 Jellyfin.Data/Entities/TrickplayInfo.cs
create mode 100644 Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs
create mode 100644 Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs
create mode 100644 Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs
create mode 100644 Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
delete mode 100644 MediaBrowser.Model/Entities/TrickplayTilesInfo.cs
delete mode 100644 MediaBrowser.Providers/Trickplay/TrickplayManager.cs
(limited to 'MediaBrowser.Controller')
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 1e0bb0cd6..7969577bc 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -78,7 +78,6 @@ 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;
@@ -97,7 +96,6 @@ 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;
@@ -593,7 +591,6 @@ 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 8ec24522b..d1fbea95a 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -26,7 +26,6 @@ using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Extensions;
-using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
@@ -48,7 +47,6 @@ 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
@@ -384,8 +382,6 @@ 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,
@@ -2138,126 +2134,6 @@ 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 1687fa442..933b95df0 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -22,6 +22,7 @@ using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
@@ -52,6 +53,7 @@ namespace Emby.Server.Implementations.Dto
private readonly Lazy _livetvManagerFactory;
private readonly ILyricManager _lyricManager;
+ private readonly ITrickplayManager _trickplayManager;
public DtoService(
ILogger logger,
@@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto
IApplicationHost appHost,
IMediaSourceManager mediaSourceManager,
Lazy livetvManagerFactory,
- ILyricManager lyricManager)
+ ILyricManager lyricManager,
+ ITrickplayManager trickplayManager)
{
_logger = logger;
_libraryManager = libraryManager;
@@ -75,6 +78,7 @@ namespace Emby.Server.Implementations.Dto
_mediaSourceManager = mediaSourceManager;
_livetvManagerFactory = livetvManagerFactory;
_lyricManager = lyricManager;
+ _trickplayManager = trickplayManager;
}
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@@ -1060,9 +1064,11 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.Trickplay))
{
+ var manifest = _trickplayManager.GetTrickplayManifest(item).ConfigureAwait(false).GetAwaiter().GetResult();
+
// To stay consistent with other fields, this must go from a Guid to a non-dashed string.
// This does not seem to occur automatically to dictionaries like it does with other Guid fields.
- dto.Trickplay = _itemRepo.GetTrickplayManifest(item).ToDictionary(x => x.Key.ToString("N", CultureInfo.InvariantCulture), y => y.Value);
+ dto.Trickplay = manifest.ToDictionary(x => x.Key.ToString("N", CultureInfo.InvariantCulture), y => y.Value);
}
if (video.ExtraType.HasValue)
diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs
index 36464d726..e4f8f076e 100644
--- a/Jellyfin.Api/Controllers/TrickplayController.cs
+++ b/Jellyfin.Api/Controllers/TrickplayController.cs
@@ -2,6 +2,7 @@ using System;
using System.ComponentModel.DataAnnotations;
using System.Net.Mime;
using System.Text;
+using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
using MediaBrowser.Controller.Library;
@@ -42,18 +43,18 @@ public class TrickplayController : BaseJellyfinApiController
/// The item id.
/// The width of a single tile.
/// The media version id, if using an alternate version.
- /// Tiles stream returned.
- /// A containing the trickplay tiles file.
+ /// Tiles playlist returned.
+ /// A containing the trickplay playlist file.
[HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesPlaylistFile]
- public ActionResult GetTrickplayHlsPlaylist(
+ public async Task GetTrickplayHlsPlaylist(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] int width,
[FromQuery] Guid? mediaSourceId)
{
- string? playlist = _trickplayManager.GetHlsPlaylist(mediaSourceId ?? itemId, width, User.GetToken());
+ string? playlist = await _trickplayManager.GetHlsPlaylist(mediaSourceId ?? itemId, width, User.GetToken()).ConfigureAwait(false);
if (string.IsNullOrEmpty(playlist))
{
@@ -64,20 +65,20 @@ public class TrickplayController : BaseJellyfinApiController
}
///
- /// Gets a trickplay tile grid image.
+ /// Gets a trickplay tile image.
///
/// The item id.
/// The width of a single tile.
- /// The index of the desired tile grid.
+ /// The index of the desired tile.
/// The media version id, if using an alternate version.
- /// Tiles image returned.
- /// Tiles image not found at specified index.
+ /// Tile image returned.
+ /// Tile image not found at specified index.
/// A containing the trickplay tiles image.
[HttpGet("Videos/{itemId}/Trickplay/{width}/{index}.jpg")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
- public ActionResult GetTrickplayGridImage(
+ public ActionResult GetTrickplayTileImage(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] int width,
[FromRoute, Required] int index,
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index b1657aeae..bd3091055 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -308,8 +308,8 @@ public class DynamicHlsHelper
if (!isLiveStream && (state.VideoRequest?.EnableTrickplay).GetValueOrDefault(false))
{
var sourceId = Guid.Parse(state.Request.MediaSourceId);
- var tilesResolutions = _trickplayManager.GetTilesResolutions(sourceId);
- AddTrickplay(state, tilesResolutions, builder, _httpContextAccessor.HttpContext.User);
+ var trickplayResolutions = await _trickplayManager.GetTrickplayResolutions(sourceId).ConfigureAwait(false);
+ AddTrickplay(state, trickplayResolutions, builder, _httpContextAccessor.HttpContext.User);
}
return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
@@ -544,17 +544,17 @@ public class DynamicHlsHelper
/// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution.
///
/// StreamState of the current stream.
- /// Dictionary of widths to corresponding tiles info.
+ /// Dictionary of widths to corresponding tiles info.
/// StringBuilder to append the field to.
/// Http user context.
- private void AddTrickplay(StreamState state, Dictionary tilesResolutions, StringBuilder builder, ClaimsPrincipal user)
+ private void AddTrickplay(StreamState state, Dictionary trickplayResolutions, StringBuilder builder, ClaimsPrincipal user)
{
const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3}\"";
- foreach (var resolution in tilesResolutions)
+ foreach (var resolution in trickplayResolutions)
{
var width = resolution.Key;
- var tilesInfo = resolution.Value;
+ var trickplayInfo = resolution.Value;
var url = string.Format(
CultureInfo.InvariantCulture,
@@ -566,9 +566,9 @@ public class DynamicHlsHelper
var line = string.Format(
CultureInfo.InvariantCulture,
playlistFormat,
- tilesInfo.Bandwidth.ToString(CultureInfo.InvariantCulture),
- tilesInfo.Width.ToString(CultureInfo.InvariantCulture),
- tilesInfo.Height.ToString(CultureInfo.InvariantCulture),
+ trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture),
+ trickplayInfo.Width.ToString(CultureInfo.InvariantCulture),
+ trickplayInfo.Height.ToString(CultureInfo.InvariantCulture),
url);
builder.AppendLine(line);
diff --git a/Jellyfin.Data/Entities/TrickplayInfo.cs b/Jellyfin.Data/Entities/TrickplayInfo.cs
new file mode 100644
index 000000000..64e7da1b5
--- /dev/null
+++ b/Jellyfin.Data/Entities/TrickplayInfo.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace Jellyfin.Data.Entities;
+
+///
+/// An entity representing the metadata for a group of trickplay tiles.
+///
+public class TrickplayInfo
+{
+ ///
+ /// Gets or sets the id of the associated item.
+ ///
+ ///
+ /// Required.
+ ///
+ [JsonIgnore]
+ public Guid ItemId { get; set; }
+
+ ///
+ /// Gets or sets width of an individual thumbnail.
+ ///
+ ///
+ /// Required.
+ ///
+ public int Width { get; set; }
+
+ ///
+ /// Gets or sets height of an individual thumbnail.
+ ///
+ ///
+ /// Required.
+ ///
+ public int Height { get; set; }
+
+ ///
+ /// Gets or sets amount of thumbnails per row.
+ ///
+ ///
+ /// Required.
+ ///
+ public int TileWidth { get; set; }
+
+ ///
+ /// Gets or sets amount of thumbnails per column.
+ ///
+ ///
+ /// Required.
+ ///
+ public int TileHeight { get; set; }
+
+ ///
+ /// Gets or sets total amount of non-black thumbnails.
+ ///
+ ///
+ /// Required.
+ ///
+ public int ThumbnailCount { get; set; }
+
+ ///
+ /// Gets or sets interval in milliseconds between each trickplay thumbnail.
+ ///
+ ///
+ /// Required.
+ ///
+ public int Interval { get; set; }
+
+ ///
+ /// Gets or sets peak bandwith usage in bits per second.
+ ///
+ ///
+ /// Required.
+ ///
+ public int Bandwidth { get; set; }
+}
diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
index 0d91707e3..ea99af004 100644
--- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
@@ -78,6 +78,11 @@ public class JellyfinDbContext : DbContext
///
public DbSet Users => Set();
+ ///
+ /// Gets the containing the trickplay metadata.
+ ///
+ public DbSet TrickplayInfos => Set();
+
/*public DbSet Artwork => Set();
public DbSet Books => Set();
diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs
new file mode 100644
index 000000000..28baf1992
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs
@@ -0,0 +1,681 @@
+//
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20230626233818_AddTrickplayInfos")]
+ partial class AddTrickplayInfos
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.7");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs
new file mode 100644
index 000000000..76b12de08
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs
@@ -0,0 +1,40 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ ///
+ public partial class AddTrickplayInfos : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "TrickplayInfos",
+ columns: table => new
+ {
+ ItemId = table.Column(type: "TEXT", nullable: false),
+ Width = table.Column(type: "INTEGER", nullable: false),
+ Height = table.Column(type: "INTEGER", nullable: false),
+ TileWidth = table.Column(type: "INTEGER", nullable: false),
+ TileHeight = table.Column(type: "INTEGER", nullable: false),
+ ThumbnailCount = table.Column(type: "INTEGER", nullable: false),
+ Interval = table.Column(type: "INTEGER", nullable: false),
+ Bandwidth = table.Column(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_TrickplayInfos", x => new { x.ItemId, x.Width });
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "TrickplayInfos");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index d23508096..3c06e1cfc 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
@@ -1,4 +1,4 @@
-//
+//
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
- modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.7");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@@ -442,6 +442,37 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("DeviceOptions");
});
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Property("Id")
diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs b/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs
new file mode 100644
index 000000000..dc1c17e5e
--- /dev/null
+++ b/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs
@@ -0,0 +1,18 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+ ///
+ /// FluentAPI configuration for the TrickplayInfo entity.
+ ///
+ public class TrickplayInfoConfiguration : IEntityTypeConfiguration
+ {
+ ///
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.HasKey(info => new { info.ItemId, info.Width });
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
new file mode 100644
index 000000000..34b27e21a
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -0,0 +1,468 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.Trickplay;
+
+///
+/// ITrickplayManager implementation.
+///
+public class TrickplayManager : ITrickplayManager
+{
+ private readonly ILogger _logger;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IFileSystem _fileSystem;
+ private readonly EncodingHelper _encodingHelper;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _config;
+ private readonly IImageEncoder _imageEncoder;
+ private readonly IDbContextFactory _dbProvider;
+
+ private static readonly SemaphoreSlim _resourcePool = new(1, 1);
+ private static readonly string[] _trickplayImgExtensions = { ".jpg" };
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger.
+ /// The media encoder.
+ /// The file systen.
+ /// The encoding helper.
+ /// The library manager.
+ /// The server configuration manager.
+ /// The image encoder.
+ /// The database provider.
+ public TrickplayManager(
+ ILogger logger,
+ IMediaEncoder mediaEncoder,
+ IFileSystem fileSystem,
+ EncodingHelper encodingHelper,
+ ILibraryManager libraryManager,
+ IServerConfigurationManager config,
+ IImageEncoder imageEncoder,
+ IDbContextFactory dbProvider)
+ {
+ _logger = logger;
+ _mediaEncoder = mediaEncoder;
+ _fileSystem = fileSystem;
+ _encodingHelper = encodingHelper;
+ _libraryManager = libraryManager;
+ _config = config;
+ _imageEncoder = imageEncoder;
+ _dbProvider = dbProvider;
+ }
+
+ ///
+ 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)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ await RefreshTrickplayDataInternal(
+ video,
+ replace,
+ width,
+ options,
+ cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private async Task RefreshTrickplayDataInternal(
+ Video video,
+ bool replace,
+ int width,
+ TrickplayOptions options,
+ CancellationToken cancellationToken)
+ {
+ if (!CanGenerateTrickplay(video, options.Interval))
+ {
+ return;
+ }
+
+ var imgTempDir = string.Empty;
+ var outputDir = GetTrickplayDirectory(video, width);
+
+ await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).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;
+ }
+
+ 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, _trickplayImgExtensions, false, false)
+ .Select(i => i.FullName)
+ .OrderBy(i => i)
+ .ToList();
+
+ // Create tiles
+ var tilesTempDir = Path.Combine(imgTempDir, Guid.NewGuid().ToString("N"));
+ var trickplayInfo = CreateTiles(images, width, options, tilesTempDir, outputDir);
+
+ // Save tiles info
+ try
+ {
+ if (trickplayInfo is not null)
+ {
+ trickplayInfo.ItemId = video.Id;
+ await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false);
+
+ _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
+ }
+ else
+ {
+ throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
+ }
+ }
+ catch (Exception ex)
+ {
+ _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);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error creating trickplay images.");
+ }
+ finally
+ {
+ _resourcePool.Release();
+
+ if (!string.IsNullOrEmpty(imgTempDir))
+ {
+ Directory.Delete(imgTempDir, true);
+ }
+ }
+ }
+
+ private TrickplayInfo CreateTiles(List images, int width, TrickplayOptions options, string workDir, string outputDir)
+ {
+ if (images.Count == 0)
+ {
+ throw new ArgumentException("Can't create trickplay from 0 images.");
+ }
+
+ Directory.CreateDirectory(workDir);
+
+ var trickplayInfo = new TrickplayInfo
+ {
+ Width = width,
+ Interval = options.Interval,
+ TileWidth = options.TileWidth,
+ TileHeight = options.TileHeight,
+ ThumbnailCount = images.Count,
+ // Set during image generation
+ Height = 0,
+ Bandwidth = 0
+ };
+
+ /*
+ * Generate trickplay tiles from sets of thumbnails
+ */
+ var imageOptions = new ImageCollageOptions
+ {
+ Width = trickplayInfo.TileWidth,
+ Height = trickplayInfo.TileHeight
+ };
+
+ var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
+ var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile);
+
+ for (int i = 0; i < requiredTiles; i++)
+ {
+ // Set output/input paths
+ var tilePath = Path.Combine(workDir, $"{i}.jpg");
+
+ imageOptions.OutputPath = tilePath;
+ imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(thumbnailsPerTile).ToList();
+
+ // Generate image and use returned height for tiles info
+ var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
+ if (trickplayInfo.Height == 0)
+ {
+ trickplayInfo.Height = height;
+ }
+
+ // Update bitrate
+ var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tilePath).Length * 8 / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000));
+ trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
+ }
+
+ /*
+ * Move trickplay tiles to output directory
+ */
+ Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName);
+
+ // Replace existing tiles if they already exist
+ if (Directory.Exists(outputDir))
+ {
+ Directory.Delete(outputDir, true);
+ }
+
+ MoveDirectory(workDir, outputDir);
+
+ return trickplayInfo;
+ }
+
+ 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;
+ }
+
+ if (video.IsShortcut)
+ {
+ return false;
+ }
+
+ if (!video.IsCompleteMedia)
+ {
+ return false;
+ }
+
+ if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks)
+ {
+ return false;
+ }
+
+ var libraryOptions = _libraryManager.GetLibraryOptions(video);
+ if (libraryOptions is null || !libraryOptions.EnableTrickplayImageExtraction)
+ {
+ return false;
+ }
+
+ // Can't extract images if there are no video streams
+ return video.GetMediaStreams().Count > 0;
+ }
+
+ ///
+ public async Task> GetTrickplayResolutions(Guid itemId)
+ {
+ var trickplayResolutions = new Dictionary();
+
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ var trickplayInfos = await dbContext.TrickplayInfos
+ .AsNoTracking()
+ .Where(i => i.ItemId.Equals(itemId))
+ .ToListAsync()
+ .ConfigureAwait(false);
+
+ foreach (var info in trickplayInfos)
+ {
+ trickplayResolutions[info.Width] = info;
+ }
+ }
+
+ return trickplayResolutions;
+ }
+
+ ///
+ public async Task SaveTrickplayInfo(TrickplayInfo info)
+ {
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ var oldInfo = await dbContext.TrickplayInfos.FindAsync(info.ItemId, info.Width).ConfigureAwait(false);
+ if (oldInfo is not null)
+ {
+ dbContext.TrickplayInfos.Remove(oldInfo);
+ }
+
+ dbContext.Add(info);
+
+ await dbContext.SaveChangesAsync().ConfigureAwait(false);
+ }
+ }
+
+ ///
+ public async Task>> GetTrickplayManifest(BaseItem item)
+ {
+ var trickplayManifest = new Dictionary>();
+ foreach (var mediaSource in item.GetMediaSources(false))
+ {
+ var mediaSourceId = Guid.Parse(mediaSource.Id);
+ var trickplayResolutions = await GetTrickplayResolutions(mediaSourceId).ConfigureAwait(false);
+
+ if (trickplayResolutions.Count > 0)
+ {
+ trickplayManifest[mediaSourceId] = trickplayResolutions;
+ }
+ }
+
+ return trickplayManifest;
+ }
+
+ ///
+ public string GetTrickplayTilePath(BaseItem item, int width, int index)
+ {
+ return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg");
+ }
+
+ ///
+ public async Task GetHlsPlaylist(Guid itemId, int width, string? apiKey)
+ {
+ var trickplayResolutions = await GetTrickplayResolutions(itemId).ConfigureAwait(false);
+ if (trickplayResolutions is not null && trickplayResolutions.TryGetValue(width, out var trickplayInfo))
+ {
+ var builder = new StringBuilder(128);
+
+ if (trickplayInfo.ThumbnailCount > 0)
+ {
+ const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}";
+ const string decimalFormat = "{0:0.###}";
+
+ var resolution = $"{trickplayInfo.Width}x{trickplayInfo.Height}";
+ var layout = $"{trickplayInfo.TileWidth}x{trickplayInfo.TileHeight}";
+ var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
+ var thumbnailDuration = trickplayInfo.Interval / 1000d;
+ var infDuration = thumbnailDuration * thumbnailsPerTile;
+ var tileCount = (int)Math.Ceiling((decimal)trickplayInfo.ThumbnailCount / thumbnailsPerTile);
+
+ builder
+ .AppendLine("#EXTM3U")
+ .Append("#EXT-X-TARGETDURATION:")
+ .AppendLine(tileCount.ToString(CultureInfo.InvariantCulture))
+ .AppendLine("#EXT-X-VERSION:7")
+ .AppendLine("#EXT-X-MEDIA-SEQUENCE:1")
+ .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
+ .AppendLine("#EXT-X-IMAGES-ONLY");
+
+ for (int i = 0; i < tileCount; i++)
+ {
+ // All tiles prior to the last must contain full amount of thumbnails (no black).
+ if (i == tileCount - 1)
+ {
+ thumbnailsPerTile = trickplayInfo.ThumbnailCount - (i * thumbnailsPerTile);
+ infDuration = thumbnailDuration * thumbnailsPerTile;
+ }
+
+ // EXTINF
+ builder
+ .Append("#EXTINF:")
+ .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, infDuration)
+ .AppendLine(",");
+
+ // EXT-X-TILES
+ builder
+ .Append("#EXT-X-TILES:RESOLUTION=")
+ .Append(resolution)
+ .Append(",LAYOUT=")
+ .Append(layout)
+ .Append(",DURATION=")
+ .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, thumbnailDuration)
+ .AppendLine();
+
+ // URL
+ builder
+ .AppendFormat(
+ CultureInfo.InvariantCulture,
+ urlFormat,
+ width.ToString(CultureInfo.InvariantCulture),
+ i.ToString(CultureInfo.InvariantCulture),
+ itemId.ToString("N"),
+ apiKey)
+ .AppendLine();
+ }
+
+ builder.AppendLine("#EXT-X-ENDLIST");
+ return builder.ToString();
+ }
+ }
+
+ return null;
+ }
+
+ 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
+ {
+ Directory.Move(source, destination);
+ }
+ 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);
+ }
+ }
+}
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 939376dd8..18d924aa8 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -11,6 +11,7 @@ using Jellyfin.Server.Implementations.Activity;
using Jellyfin.Server.Implementations.Devices;
using Jellyfin.Server.Implementations.Events;
using Jellyfin.Server.Implementations.Security;
+using Jellyfin.Server.Implementations.Trickplay;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Controller;
using MediaBrowser.Controller.BaseItemManager;
@@ -21,6 +22,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
+using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Activity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -77,6 +79,7 @@ namespace Jellyfin.Server
serviceCollection.AddSingleton();
serviceCollection.AddScoped();
serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
// TODO search the assemblies instead of adding them manually?
serviceCollection.AddSingleton();
diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
index 42c680761..c7bfbdb53 100644
--- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs
+++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Drawing;
namespace MediaBrowser.Controller.Drawing
@@ -84,13 +83,13 @@ namespace MediaBrowser.Controller.Drawing
void CreateSplashscreen(IReadOnlyList posters, IReadOnlyList backdrops);
///
- /// Creates a new jpeg trickplay grid image.
+ /// Creates a new trickplay tile image.
///
- /// The options to use when creating the image. Width and Height are a quantity of tiles in this case, not pixels.
+ /// The options to use when creating the image. Width and Height are a quantity of thumbnails in this case, not pixels.
/// The image encode quality.
- /// The width of a single trickplay image.
- /// Optional height of a single trickplay image, if it is known.
- /// Height of single decoded trickplay image.
- int CreateTrickplayGrid(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight);
+ /// The width of a single trickplay thumbnail.
+ /// Optional height of a single trickplay thumbnail, if it is known.
+ /// Height of single decoded trickplay thumbnail.
+ int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight);
}
}
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index 11eb4932c..2c52b2b45 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -61,27 +61,6 @@ namespace MediaBrowser.Controller.Persistence
/// The list of chapters to save.
void SaveChapters(Guid id, IReadOnlyList chapters);
- ///