mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-27 01:50:53 +01:00
Enhance Trickplay (#11883)
This commit is contained in:
@@ -76,7 +76,65 @@ public class TrickplayManager : ITrickplayManager
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken)
|
||||
public async Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions? libraryOptions, CancellationToken cancellationToken)
|
||||
{
|
||||
var options = _config.Configuration.TrickplayOptions;
|
||||
if (!CanGenerateTrickplay(video, options.Interval))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var existingTrickplayResolutions = await GetTrickplayResolutions(video.Id).ConfigureAwait(false);
|
||||
foreach (var resolution in existingTrickplayResolutions)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var existingResolution = resolution.Key;
|
||||
var tileWidth = resolution.Value.TileWidth;
|
||||
var tileHeight = resolution.Value.TileHeight;
|
||||
var shouldBeSavedWithMedia = libraryOptions is null ? false : libraryOptions.SaveTrickplayWithMedia;
|
||||
var localOutputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, existingResolution, false);
|
||||
var mediaOutputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, existingResolution, true);
|
||||
if (shouldBeSavedWithMedia && Directory.Exists(localOutputDir))
|
||||
{
|
||||
var localDirFiles = Directory.GetFiles(localOutputDir);
|
||||
var mediaDirExists = Directory.Exists(mediaOutputDir);
|
||||
if (localDirFiles.Length > 0 && ((mediaDirExists && Directory.GetFiles(mediaOutputDir).Length == 0) || !mediaDirExists))
|
||||
{
|
||||
// Move images from local dir to media dir
|
||||
MoveContent(localOutputDir, mediaOutputDir);
|
||||
_logger.LogInformation("Moved trickplay images for {ItemName} to {Location}", video.Name, mediaOutputDir);
|
||||
}
|
||||
}
|
||||
else if (Directory.Exists(mediaOutputDir))
|
||||
{
|
||||
var mediaDirFiles = Directory.GetFiles(mediaOutputDir);
|
||||
var localDirExists = Directory.Exists(localOutputDir);
|
||||
if (mediaDirFiles.Length > 0 && ((localDirExists && Directory.GetFiles(localOutputDir).Length == 0) || !localDirExists))
|
||||
{
|
||||
// Move images from media dir to local dir
|
||||
MoveContent(mediaOutputDir, localOutputDir);
|
||||
_logger.LogInformation("Moved trickplay images for {ItemName} to {Location}", video.Name, localOutputDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveContent(string sourceFolder, string destinationFolder)
|
||||
{
|
||||
_fileSystem.MoveDirectory(sourceFolder, destinationFolder);
|
||||
var parent = Directory.GetParent(sourceFolder);
|
||||
if (parent is not null)
|
||||
{
|
||||
var parentContent = Directory.GetDirectories(parent.FullName);
|
||||
if (parentContent.Length == 0)
|
||||
{
|
||||
Directory.Delete(parent.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions? libraryOptions, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
|
||||
|
||||
@@ -95,6 +153,7 @@ public class TrickplayManager : ITrickplayManager
|
||||
replace,
|
||||
width,
|
||||
options,
|
||||
libraryOptions,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -104,6 +163,7 @@ public class TrickplayManager : ITrickplayManager
|
||||
bool replace,
|
||||
int width,
|
||||
TrickplayOptions options,
|
||||
LibraryOptions? libraryOptions,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!CanGenerateTrickplay(video, options.Interval))
|
||||
@@ -144,14 +204,53 @@ public class TrickplayManager : ITrickplayManager
|
||||
actualWidth = 2 * ((int)mediaSource.VideoStream.Width / 2);
|
||||
}
|
||||
|
||||
var outputDir = GetTrickplayDirectory(video, actualWidth);
|
||||
var tileWidth = options.TileWidth;
|
||||
var tileHeight = options.TileHeight;
|
||||
var saveWithMedia = libraryOptions is null ? false : libraryOptions.SaveTrickplayWithMedia;
|
||||
var outputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, actualWidth, saveWithMedia);
|
||||
|
||||
if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(actualWidth))
|
||||
// Import existing trickplay tiles
|
||||
if (!replace && Directory.Exists(outputDir))
|
||||
{
|
||||
_logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting", video.Id);
|
||||
return;
|
||||
var existingFiles = Directory.GetFiles(outputDir);
|
||||
if (existingFiles.Length > 0)
|
||||
{
|
||||
var hasTrickplayResolution = await HasTrickplayResolutionAsync(video.Id, actualWidth).ConfigureAwait(false);
|
||||
if (hasTrickplayResolution)
|
||||
{
|
||||
_logger.LogDebug("Found existing trickplay files for {ItemId}.", video.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Import tiles
|
||||
var localTrickplayInfo = new TrickplayInfo
|
||||
{
|
||||
ItemId = video.Id,
|
||||
Width = width,
|
||||
Interval = options.Interval,
|
||||
TileWidth = options.TileWidth,
|
||||
TileHeight = options.TileHeight,
|
||||
ThumbnailCount = existingFiles.Length,
|
||||
Height = 0,
|
||||
Bandwidth = 0
|
||||
};
|
||||
|
||||
foreach (var tile in existingFiles)
|
||||
{
|
||||
var image = _imageEncoder.GetImageSize(tile);
|
||||
localTrickplayInfo.Height = Math.Max(localTrickplayInfo.Height, image.Height);
|
||||
var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tile).Length * 8 / localTrickplayInfo.TileWidth / localTrickplayInfo.TileHeight / (localTrickplayInfo.Interval / 1000));
|
||||
localTrickplayInfo.Bandwidth = Math.Max(localTrickplayInfo.Bandwidth, bitrate);
|
||||
}
|
||||
|
||||
await SaveTrickplayInfo(localTrickplayInfo).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug("Imported existing trickplay files for {ItemId}.", video.Id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate trickplay tiles
|
||||
var mediaStream = mediaSource.VideoStream;
|
||||
var container = mediaSource.Container;
|
||||
|
||||
@@ -224,7 +323,7 @@ public class TrickplayManager : ITrickplayManager
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TrickplayInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string outputDir)
|
||||
public TrickplayInfo CreateTiles(IReadOnlyList<string> images, int width, TrickplayOptions options, string outputDir)
|
||||
{
|
||||
if (images.Count == 0)
|
||||
{
|
||||
@@ -264,7 +363,7 @@ public class TrickplayManager : ITrickplayManager
|
||||
var tilePath = Path.Combine(workDir, $"{i}.jpg");
|
||||
|
||||
imageOptions.OutputPath = tilePath;
|
||||
imageOptions.InputPaths = images.GetRange(i * thumbnailsPerTile, Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile)));
|
||||
imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(Math.Min(thumbnailsPerTile, images.Count - (i * 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);
|
||||
@@ -289,7 +388,7 @@ public class TrickplayManager : ITrickplayManager
|
||||
Directory.Delete(outputDir, true);
|
||||
}
|
||||
|
||||
MoveDirectory(workDir, outputDir);
|
||||
_fileSystem.MoveDirectory(workDir, outputDir);
|
||||
|
||||
return trickplayInfo;
|
||||
}
|
||||
@@ -355,6 +454,24 @@ public class TrickplayManager : ITrickplayManager
|
||||
return trickplayResolutions;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Guid>> GetTrickplayItemsAsync()
|
||||
{
|
||||
List<Guid> trickplayItems;
|
||||
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
trickplayItems = await dbContext.TrickplayInfos
|
||||
.AsNoTracking()
|
||||
.Select(i => i.ItemId)
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return trickplayItems;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SaveTrickplayInfo(TrickplayInfo info)
|
||||
{
|
||||
@@ -392,9 +509,15 @@ public class TrickplayManager : ITrickplayManager
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetTrickplayTilePath(BaseItem item, int width, int index)
|
||||
public async Task<string> GetTrickplayTilePathAsync(BaseItem item, int width, int index, bool saveWithMedia)
|
||||
{
|
||||
return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg");
|
||||
var trickplayResolutions = await GetTrickplayResolutions(item.Id).ConfigureAwait(false);
|
||||
if (trickplayResolutions is not null && trickplayResolutions.TryGetValue(width, out var trickplayInfo))
|
||||
{
|
||||
return Path.Combine(GetTrickplayDirectory(item, trickplayInfo.TileWidth, trickplayInfo.TileHeight, width, saveWithMedia), index + ".jpg");
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -470,29 +593,33 @@ public class TrickplayManager : ITrickplayManager
|
||||
return null;
|
||||
}
|
||||
|
||||
private string GetTrickplayDirectory(BaseItem item, int? width = null)
|
||||
/// <inheritdoc />
|
||||
public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false)
|
||||
{
|
||||
var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay");
|
||||
var path = saveWithMedia
|
||||
? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
|
||||
: Path.Combine(item.GetInternalMetadataPath(), "trickplay");
|
||||
|
||||
return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
|
||||
var subdirectory = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0} - {1}x{2}",
|
||||
width.ToString(CultureInfo.InvariantCulture),
|
||||
tileWidth.ToString(CultureInfo.InvariantCulture),
|
||||
tileHeight.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
return Path.Combine(path, subdirectory);
|
||||
}
|
||||
|
||||
private void MoveDirectory(string source, string destination)
|
||||
private async Task<bool> HasTrickplayResolutionAsync(Guid itemId, int width)
|
||||
{
|
||||
try
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
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);
|
||||
return await dbContext.TrickplayInfos
|
||||
.AsNoTracking()
|
||||
.Where(i => i.ItemId.Equals(itemId))
|
||||
.AnyAsync(i => i.Width == width)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user