pull/13771/merge
Tim Eisele 3 weeks ago committed by GitHub
commit a9462df25a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -505,6 +505,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
serviceCollection.AddSingleton<IMediaStreamRepository, MediaStreamRepository>();
serviceCollection.AddSingleton<IKeyframeRepository, KeyframeRepository>();
serviceCollection.AddSingleton<IItemTypeLookup, ItemTypeLookup>();
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();

@ -1421,6 +1421,7 @@ public class DynamicHlsController : BaseJellyfinApiController
.ConfigureAwait(false);
var request = new CreateMainPlaylistRequest(
Guid.Parse(state.BaseRequest.MediaSourceId),
state.MediaPath,
state.SegmentLength * 1000,
state.RunTimeTicks ?? 0,

@ -114,6 +114,7 @@ public sealed class BaseItemRepository
context.ItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
context.KeyframeData.Where(e => e.ItemId == id).ExecuteDelete();
context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete();
context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Persistence;
using Microsoft.EntityFrameworkCore;
namespace Jellyfin.Server.Implementations.Item;
/// <summary>
/// Repository for obtaining Keyframe data.
/// </summary>
public class KeyframeRepository : IKeyframeRepository
{
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
/// <summary>
/// Initializes a new instance of the <see cref="KeyframeRepository"/> class.
/// </summary>
/// <param name="dbProvider">The EFCore db factory.</param>
public KeyframeRepository(IDbContextFactory<JellyfinDbContext> dbProvider)
{
_dbProvider = dbProvider;
}
private static MediaEncoding.Keyframes.KeyframeData Map(KeyframeData entity)
{
return new MediaEncoding.Keyframes.KeyframeData(
entity.TotalDuration,
(entity.KeyframeTicks ?? []).ToList());
}
private KeyframeData Map(MediaEncoding.Keyframes.KeyframeData dto, Guid itemId)
{
return new()
{
ItemId = itemId,
TotalDuration = dto.TotalDuration,
KeyframeTicks = dto.KeyframeTicks.ToList()
};
}
/// <inheritdoc />
public IReadOnlyList<MediaEncoding.Keyframes.KeyframeData> GetKeyframeData(Guid itemId)
{
using var context = _dbProvider.CreateDbContext();
return context.KeyframeData.AsNoTracking().Where(e => e.ItemId.Equals(itemId)).Select(e => Map(e)).ToList();
}
/// <inheritdoc />
public async Task SaveKeyframeDataAsync(Guid itemId, MediaEncoding.Keyframes.KeyframeData data, CancellationToken cancellationToken)
{
using var context = _dbProvider.CreateDbContext();
using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
await context.KeyframeData.AddAsync(Map(data, itemId), cancellationToken).ConfigureAwait(false);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}
}

@ -56,6 +56,7 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.MigrateLibraryDb),
typeof(Routines.MigrateRatingLevels),
typeof(Routines.MoveTrickplayFiles),
typeof(Routines.MigrateKeyframeData),
};
/// <summary>

@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// Migration to move extracted files to the new directories.
/// </summary>
public class MigrateKeyframeData : IDatabaseMigrationRoutine
{
private readonly ILibraryManager _libraryManager;
private readonly ILogger<MoveTrickplayFiles> _logger;
private readonly IApplicationPaths _appPaths;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
/// <summary>
/// Initializes a new instance of the <see cref="MigrateKeyframeData"/> class.
/// </summary>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="logger">The logger.</param>
/// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="dbProvider">The EFCore db factory.</param>
public MigrateKeyframeData(
ILibraryManager libraryManager,
ILogger<MoveTrickplayFiles> logger,
IApplicationPaths appPaths,
IDbContextFactory<JellyfinDbContext> dbProvider)
{
_libraryManager = libraryManager;
_logger = logger;
_appPaths = appPaths;
_dbProvider = dbProvider;
}
private string KeyframeCachePath => Path.Combine(_appPaths.DataPath, "keyframes");
/// <inheritdoc />
public Guid Id => new("EA4bCAE1-09A4-428E-9B90-4B4FD2EA1B24");
/// <inheritdoc />
public string Name => "MigrateKeyframeData";
/// <inheritdoc />
public bool PerformOnNewInstall => false;
/// <inheritdoc />
public void Perform()
{
const int Limit = 100;
int itemCount = 0, offset = 0, previousCount;
var sw = Stopwatch.StartNew();
var itemsQuery = new InternalItemsQuery
{
MediaTypes = [MediaType.Video],
SourceTypes = [SourceType.Library],
IsVirtualItem = false,
IsFolder = false
};
using var context = _dbProvider.CreateDbContext();
using var transaction = context.Database.BeginTransaction();
List<KeyframeData> keyframes = [];
do
{
var result = _libraryManager.GetItemsResult(itemsQuery);
_logger.LogInformation("Importing keyframes for {Count} items", result.TotalRecordCount);
var items = result.Items;
previousCount = items.Count;
offset += Limit;
foreach (var item in items)
{
if (TryGetKeyframeData(item, out var data))
{
keyframes.Add(data);
}
if (++itemCount % 10_000 == 0)
{
context.KeyframeData.AddRange(keyframes);
keyframes.Clear();
_logger.LogInformation("Imported keyframes for {Count} items in {Time}", itemCount, sw.Elapsed);
}
}
} while (previousCount == Limit);
context.KeyframeData.AddRange(keyframes);
context.SaveChanges();
transaction.Commit();
_logger.LogInformation("Imported keyframes for {Count} items in {Time}", itemCount, sw.Elapsed);
Directory.Delete(KeyframeCachePath, true);
}
private bool TryGetKeyframeData(BaseItem item, [NotNullWhen(true)] out KeyframeData? data)
{
data = null;
var path = item.Path;
if (!string.IsNullOrEmpty(path))
{
var cachePath = GetCachePath(KeyframeCachePath, path);
if (TryReadFromCache(cachePath, out var keyframeData))
{
data = new()
{
ItemId = item.Id,
KeyframeTicks = keyframeData.KeyframeTicks.ToList(),
TotalDuration = keyframeData.TotalDuration
};
return true;
}
}
return false;
}
private string? GetCachePath(string keyframeCachePath, string filePath)
{
DateTime? lastWriteTimeUtc;
try
{
lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath);
}
catch (IOException e)
{
_logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
return null;
}
ReadOnlySpan<char> filename = (filePath + "_" + lastWriteTimeUtc.Value.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5() + ".json";
var prefix = filename[..1];
return Path.Join(keyframeCachePath, prefix, filename);
}
private static bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData? cachedResult)
{
if (File.Exists(cachePath))
{
var bytes = File.ReadAllBytes(cachePath);
cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions);
return cachedResult is not null;
}
cachedResult = null;
return false;
}
}

@ -28,6 +28,7 @@
<ProjectReference Include="../Emby.Naming/Emby.Naming.csproj" />
<ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
<ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
<ProjectReference Include="../src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj" />
</ItemGroup>
<ItemGroup>

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.MediaEncoding.Keyframes;
namespace MediaBrowser.Controller.Persistence;
/// <summary>
/// Provides methods for accessing keyframe data.
/// </summary>
public interface IKeyframeRepository
{
/// <summary>
/// Gets the keyframe data.
/// </summary>
/// <param name="itemId">The item id.</param>
/// <returns>The keyframe data.</returns>
IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId);
/// <summary>
/// Saves the keyframe data.
/// </summary>
/// <param name="itemId">The item id.</param>
/// <param name="data">The keyframe data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken);
}

@ -0,0 +1,32 @@
#pragma warning disable CA2227 // Collection properties should be read only
using System;
using System.Collections.Generic;
namespace Jellyfin.Database.Implementations.Entities;
/// <summary>
/// Keyframe information for a specific file.
/// </summary>
public class KeyframeData
{
/// <summary>
/// Gets or Sets the ItemId.
/// </summary>
public required Guid ItemId { get; set; }
/// <summary>
/// Gets or sets the total duration of the stream in ticks.
/// </summary>
public long TotalDuration { get; set; }
/// <summary>
/// Gets or sets the keyframes in ticks.
/// </summary>
public ICollection<long>? KeyframeTicks { get; set; }
/// <summary>
/// Gets or sets the item reference.
/// </summary>
public BaseItemEntity? Item { get; set; }
}

@ -157,6 +157,11 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
/// </summary>
public DbSet<BaseItemTrailerType> BaseItemTrailerTypes => Set<BaseItemTrailerType>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
public DbSet<KeyframeData> KeyframeData => Set<KeyframeData>();
/*public DbSet<Artwork> Artwork => Set<Artwork>();
public DbSet<Book> Books => Set<Book>();

@ -0,0 +1,18 @@
using Jellyfin.Database.Implementations.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Jellyfin.Database.Implementations.ModelConfiguration;
/// <summary>
/// KeyframeData Configuration.
/// </summary>
public class KeyframeDataConfiguration : IEntityTypeConfiguration<KeyframeData>
{
/// <inheritdoc/>
public void Configure(EntityTypeBuilder<KeyframeData> builder)
{
builder.HasKey(e => e.ItemId);
builder.HasOne(e => e.Item).WithMany().HasForeignKey(e => e.ItemId);
}
}

@ -0,0 +1,41 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class AddKeyframeData : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "KeyframeData",
columns: table => new
{
ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
TotalDuration = table.Column<long>(type: "INTEGER", nullable: false),
KeyframeTicks = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_KeyframeData", x => x.ItemId);
table.ForeignKey(
name: "FK_KeyframeData_BaseItems_ItemId",
column: x => x.ItemId,
principalTable: "BaseItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "KeyframeData");
}
}
}

@ -748,6 +748,24 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
{
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.PrimitiveCollection<string>("KeyframeTicks")
.HasColumnType("TEXT");
b.Property<long>("TotalDuration")
.HasColumnType("INTEGER");
b.HasKey("ItemId");
b.ToTable("KeyframeData");
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
});
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b =>
{
b.Property<Guid>("Id")
@ -1519,6 +1537,17 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Navigation("ItemValue");
});
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b =>
{
b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")
.WithMany()
.HasForeignKey("ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Item");
});
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b =>
{
b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item")

@ -1,13 +1,12 @@
#pragma warning disable CA1826 // Do not use Enumerable methods on indexable collections
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Text.Json;
using Jellyfin.Extensions.Json;
using System.Linq;
using System.Threading;
using Jellyfin.MediaEncoding.Hls.Extractors;
using Jellyfin.MediaEncoding.Keyframes;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
namespace Jellyfin.MediaEncoding.Hls.Cache;
@ -15,82 +14,48 @@ namespace Jellyfin.MediaEncoding.Hls.Cache;
/// <inheritdoc />
public class CacheDecorator : IKeyframeExtractor
{
private readonly IKeyframeRepository _keyframeRepository;
private readonly IKeyframeExtractor _keyframeExtractor;
private readonly ILogger<CacheDecorator> _logger;
private readonly string _keyframeExtractorName;
private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private readonly string _keyframeCachePath;
/// <summary>
/// Initializes a new instance of the <see cref="CacheDecorator"/> class.
/// </summary>
/// <param name="applicationPaths">An instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="keyframeRepository">An instance of the <see cref="IKeyframeRepository"/> interface.</param>
/// <param name="keyframeExtractor">An instance of the <see cref="IKeyframeExtractor"/> interface.</param>
/// <param name="logger">An instance of the <see cref="ILogger{CacheDecorator}"/> interface.</param>
public CacheDecorator(IApplicationPaths applicationPaths, IKeyframeExtractor keyframeExtractor, ILogger<CacheDecorator> logger)
public CacheDecorator(IKeyframeRepository keyframeRepository, IKeyframeExtractor keyframeExtractor, ILogger<CacheDecorator> logger)
{
ArgumentNullException.ThrowIfNull(applicationPaths);
ArgumentNullException.ThrowIfNull(keyframeRepository);
ArgumentNullException.ThrowIfNull(keyframeExtractor);
_keyframeRepository = keyframeRepository;
_keyframeExtractor = keyframeExtractor;
_logger = logger;
_keyframeExtractorName = keyframeExtractor.GetType().Name;
// TODO make the dir configurable
_keyframeCachePath = Path.Combine(applicationPaths.DataPath, "keyframes");
}
/// <inheritdoc />
public bool IsMetadataBased => _keyframeExtractor.IsMetadataBased;
/// <inheritdoc />
public bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
public bool TryExtractKeyframes(Guid itemId, string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
{
keyframeData = null;
var cachePath = GetCachePath(_keyframeCachePath, filePath);
if (TryReadFromCache(cachePath, out var cachedResult))
keyframeData = _keyframeRepository.GetKeyframeData(itemId).FirstOrDefault();
if (keyframeData is null)
{
keyframeData = cachedResult;
return true;
if (!_keyframeExtractor.TryExtractKeyframes(itemId, filePath, out var result))
{
_logger.LogDebug("Failed to extract keyframes using {ExtractorName}", _keyframeExtractorName);
return false;
}
_logger.LogDebug("Successfully extracted keyframes using {ExtractorName}", _keyframeExtractorName);
keyframeData = result;
_keyframeRepository.SaveKeyframeDataAsync(itemId, keyframeData, CancellationToken.None).GetAwaiter().GetResult();
}
if (!_keyframeExtractor.TryExtractKeyframes(filePath, out var result))
{
_logger.LogDebug("Failed to extract keyframes using {ExtractorName}", _keyframeExtractorName);
return false;
}
_logger.LogDebug("Successfully extracted keyframes using {ExtractorName}", _keyframeExtractorName);
keyframeData = result;
SaveToCache(cachePath, keyframeData);
return true;
}
private static void SaveToCache(string cachePath, KeyframeData keyframeData)
{
var json = JsonSerializer.Serialize(keyframeData, _jsonOptions);
Directory.CreateDirectory(Path.GetDirectoryName(cachePath) ?? throw new ArgumentException($"Provided path ({cachePath}) is not valid.", nameof(cachePath)));
File.WriteAllText(cachePath, json);
}
private static string GetCachePath(string keyframeCachePath, string filePath)
{
var lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath);
ReadOnlySpan<char> filename = (filePath + "_" + lastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5() + ".json";
var prefix = filename[..1];
return Path.Join(keyframeCachePath, prefix, filename);
}
private static bool TryReadFromCache(string cachePath, [NotNullWhen(true)] out KeyframeData? cachedResult)
{
if (File.Exists(cachePath))
{
var bytes = File.ReadAllBytes(cachePath);
cachedResult = JsonSerializer.Deserialize<KeyframeData>(bytes, _jsonOptions);
return cachedResult is not null;
}
cachedResult = null;
return false;
}
}

@ -34,7 +34,7 @@ public class FfProbeKeyframeExtractor : IKeyframeExtractor
public bool IsMetadataBased => false;
/// <inheritdoc />
public bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
public bool TryExtractKeyframes(Guid itemId, string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
{
if (!_namingOptions.VideoFileExtensions.Contains(Path.GetExtension(filePath.AsSpan()), StringComparison.OrdinalIgnoreCase))
{

@ -1,3 +1,4 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Jellyfin.MediaEncoding.Keyframes;
@ -16,8 +17,9 @@ public interface IKeyframeExtractor
/// <summary>
/// Attempt to extract keyframes.
/// </summary>
/// <param name="itemId">The item id.</param>
/// <param name="filePath">The path to the file.</param>
/// <param name="keyframeData">The keyframes.</param>
/// <returns>A value indicating whether the keyframe extraction was successful.</returns>
bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData);
bool TryExtractKeyframes(Guid itemId, string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData);
}

@ -24,7 +24,7 @@ public class MatroskaKeyframeExtractor : IKeyframeExtractor
public bool IsMetadataBased => true;
/// <inheritdoc />
public bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
public bool TryExtractKeyframes(Guid itemId, string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
{
if (!filePath.AsSpan().EndsWith(".mkv", StringComparison.OrdinalIgnoreCase))
{

@ -7,6 +7,7 @@
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />
<ProjectReference Include="../../MediaBrowser.Model/MediaBrowser.Model.csproj" />
<ProjectReference Include="../../MediaBrowser.Controller/MediaBrowser.Controller.csproj" />
<ProjectReference Include="../Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj" />
</ItemGroup>

@ -1,3 +1,5 @@
using System;
namespace Jellyfin.MediaEncoding.Hls.Playlist;
/// <summary>
@ -8,6 +10,7 @@ public class CreateMainPlaylistRequest
/// <summary>
/// Initializes a new instance of the <see cref="CreateMainPlaylistRequest"/> class.
/// </summary>
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="filePath">The absolute file path to the file.</param>
/// <param name="desiredSegmentLengthMs">The desired segment length in milliseconds.</param>
/// <param name="totalRuntimeTicks">The total duration of the file in ticks.</param>
@ -15,8 +18,9 @@ public class CreateMainPlaylistRequest
/// <param name="endpointPrefix">The URI prefix for the relative URL in the playlist.</param>
/// <param name="queryString">The desired query string to append (must start with ?).</param>
/// <param name="isRemuxingVideo">Whether the video is being remuxed.</param>
public CreateMainPlaylistRequest(string filePath, int desiredSegmentLengthMs, long totalRuntimeTicks, string segmentContainer, string endpointPrefix, string queryString, bool isRemuxingVideo)
public CreateMainPlaylistRequest(Guid mediaSourceId, string filePath, int desiredSegmentLengthMs, long totalRuntimeTicks, string segmentContainer, string endpointPrefix, string queryString, bool isRemuxingVideo)
{
MediaSourceId = mediaSourceId;
FilePath = filePath;
DesiredSegmentLengthMs = desiredSegmentLengthMs;
TotalRuntimeTicks = totalRuntimeTicks;
@ -26,6 +30,11 @@ public class CreateMainPlaylistRequest
IsRemuxingVideo = isRemuxingVideo;
}
/// <summary>
/// Gets the media source id.
/// </summary>
public Guid MediaSourceId { get; }
/// <summary>
/// Gets the file path.
/// </summary>

@ -35,7 +35,7 @@ public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator
{
IReadOnlyList<double> segments;
// For video transcodes it is sufficient with equal length segments as ffmpeg will create new keyframes
if (request.IsRemuxingVideo && TryExtractKeyframes(request.FilePath, out var keyframeData))
if (request.IsRemuxingVideo && TryExtractKeyframes(request.MediaSourceId, request.FilePath, out var keyframeData))
{
segments = ComputeSegments(keyframeData, request.DesiredSegmentLengthMs);
}
@ -104,7 +104,7 @@ public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator
return builder.ToString();
}
private bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
private bool TryExtractKeyframes(Guid itemId, string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
{
keyframeData = null;
if (!IsExtractionAllowedForFile(filePath, _serverConfigurationManager.GetEncodingOptions().AllowOnDemandMetadataBasedKeyframeExtractionForExtensions))
@ -116,7 +116,7 @@ public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator
for (var i = 0; i < len; i++)
{
var extractor = _extractors[i];
if (!extractor.TryExtractKeyframes(filePath, out var result))
if (!extractor.TryExtractKeyframes(itemId, filePath, out var result))
{
continue;
}

@ -9,7 +9,6 @@ using Jellyfin.MediaEncoding.Hls.Extractors;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
@ -23,7 +22,7 @@ public class KeyframeExtractionScheduledTask : IScheduledTask
private readonly ILocalizationManager _localizationManager;
private readonly ILibraryManager _libraryManager;
private readonly IKeyframeExtractor[] _keyframeExtractors;
private static readonly BaseItemKind[] _itemTypes = { BaseItemKind.Episode, BaseItemKind.Movie };
private static readonly BaseItemKind[] _itemTypes = [BaseItemKind.Episode, BaseItemKind.Movie];
/// <summary>
/// Initializes a new instance of the <see cref="KeyframeExtractionScheduledTask"/> class.
@ -55,11 +54,11 @@ public class KeyframeExtractionScheduledTask : IScheduledTask
{
var query = new InternalItemsQuery
{
MediaTypes = new[] { MediaType.Video },
MediaTypes = [MediaType.Video],
IsVirtualItem = false,
IncludeItemTypes = _itemTypes,
DtoOptions = new DtoOptions(true),
SourceTypes = new[] { SourceType.Library },
SourceTypes = [SourceType.Library],
Recursive = true,
Limit = Pagesize
};
@ -74,19 +73,16 @@ public class KeyframeExtractionScheduledTask : IScheduledTask
query.StartIndex = startIndex;
var videos = _libraryManager.GetItemList(query);
var currentPageCount = videos.Count;
// TODO parallelize with Parallel.ForEach?
for (var i = 0; i < currentPageCount; i++)
foreach (var video in videos)
{
var video = videos[i];
// Only local files supported
if (video.IsFileProtocol && File.Exists(video.Path))
var path = video.Path;
if (File.Exists(path))
{
for (var j = 0; j < _keyframeExtractors.Length; j++)
foreach (var extractor in _keyframeExtractors)
{
var extractor = _keyframeExtractors[j];
// The cache decorator will make sure to save them in the data dir
if (extractor.TryExtractKeyframes(video.Path, out _))
// The cache decorator will make sure to save the keyframes
if (extractor.TryExtractKeyframes(video.Id, path, out _))
{
break;
}
@ -107,5 +103,5 @@ public class KeyframeExtractionScheduledTask : IScheduledTask
}
/// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => Enumerable.Empty<TaskTriggerInfo>();
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => [];
}

Loading…
Cancel
Save