Refactor and add scheduled task

This commit is contained in:
cvium 2022-01-11 23:30:30 +01:00
parent c658a883a2
commit 6ffa9539bb
24 changed files with 924 additions and 744 deletions

View File

@ -47,6 +47,7 @@ using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Udp;
using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
using Jellyfin.MediaEncoding.Hls.Playlist;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
using MediaBrowser.Common;
@ -999,6 +1000,9 @@ namespace Emby.Server.Implementations
// Network
yield return typeof(NetworkManager).Assembly;
// Hls
yield return typeof(DynamicHlsPlaylistGenerator).Assembly;
foreach (var i in GetAssembliesWithPartsInternal())
{
yield return i;

View File

@ -848,7 +848,7 @@ namespace Jellyfin.Api.Controllers
StreamOptions = streamOptions
};
return await GetVariantPlaylistInternal(streamingRequest, "main", cancellationTokenSource)
return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
.ConfigureAwait(false);
}
@ -1013,7 +1013,7 @@ namespace Jellyfin.Api.Controllers
StreamOptions = streamOptions
};
return await GetVariantPlaylistInternal(streamingRequest, "main", cancellationTokenSource)
return await GetVariantPlaylistInternal(streamingRequest, cancellationTokenSource)
.ConfigureAwait(false);
}
@ -1371,7 +1371,7 @@ namespace Jellyfin.Api.Controllers
.ConfigureAwait(false);
}
private async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, string name, CancellationTokenSource cancellationTokenSource)
private async Task<ActionResult> GetVariantPlaylistInternal(StreamingRequestDto streamingRequest, CancellationTokenSource cancellationTokenSource)
{
using var state = await StreamingHelpers.GetStreamingState(
streamingRequest,

View File

@ -39,7 +39,7 @@ namespace MediaBrowser.Model.Configuration
EnableHardwareEncoding = true;
AllowHevcEncoding = false;
EnableSubtitleExtraction = true;
AllowAutomaticKeyframeExtractionForExtensions = Array.Empty<string>();
AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = Array.Empty<string>();
HardwareDecodingCodecs = new string[] { "h264", "vc1" };
}
@ -119,6 +119,6 @@ namespace MediaBrowser.Model.Configuration
public string[] HardwareDecodingCodecs { get; set; }
public string[] AllowAutomaticKeyframeExtractionForExtensions { get; set; }
public string[] AllowOnDemandMetadataBasedKeyframeExtractionForExtensions { get; set; }
}
}

View File

@ -0,0 +1,87 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Text.Json;
using Jellyfin.Extensions.Json;
using Jellyfin.MediaEncoding.Hls.Extractors;
using Jellyfin.MediaEncoding.Keyframes;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
namespace Jellyfin.MediaEncoding.Hls.Cache;
/// <inheritdoc />
public class CacheDecorator : IKeyframeExtractor
{
private readonly IKeyframeExtractor _keyframeExtractor;
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="keyframeExtractor">An instance of the <see cref="IKeyframeExtractor"/> interface.</param>
public CacheDecorator(IApplicationPaths applicationPaths, IKeyframeExtractor keyframeExtractor)
{
_keyframeExtractor = keyframeExtractor;
ArgumentNullException.ThrowIfNull(applicationPaths);
// 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)
{
keyframeData = null;
var cachePath = GetCachePath(_keyframeCachePath, filePath);
if (TryReadFromCache(cachePath, out var cachedResult))
{
keyframeData = cachedResult;
return true;
}
if (!_keyframeExtractor.TryExtractKeyframes(filePath, out var result))
{
return false;
}
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 != null;
}
cachedResult = null;
return false;
}
}

View File

@ -1,8 +1,11 @@
using Jellyfin.MediaEncoding.Hls.Playlist;
using System;
using Jellyfin.MediaEncoding.Hls.Cache;
using Jellyfin.MediaEncoding.Hls.Extractors;
using Jellyfin.MediaEncoding.Hls.Playlist;
using Microsoft.Extensions.DependencyInjection;
namespace Jellyfin.MediaEncoding.Hls.Extensions
{
namespace Jellyfin.MediaEncoding.Hls.Extensions;
/// <summary>
/// Extensions for the <see cref="IServiceCollection"/> interface.
/// </summary>
@ -15,7 +18,19 @@ namespace Jellyfin.MediaEncoding.Hls.Extensions
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddHlsPlaylistGenerator(this IServiceCollection serviceCollection)
{
return serviceCollection.AddSingleton<IDynamicHlsPlaylistGenerator, DynamicHlsPlaylistGenerator>();
}
serviceCollection.AddSingletonWithDecorator(typeof(FfProbeKeyframeExtractor));
serviceCollection.AddSingletonWithDecorator(typeof(MatroskaKeyframeExtractor));
serviceCollection.AddSingleton<IDynamicHlsPlaylistGenerator, DynamicHlsPlaylistGenerator>();
return serviceCollection;
}
private static void AddSingletonWithDecorator(this IServiceCollection serviceCollection, Type type)
{
serviceCollection.AddSingleton<IKeyframeExtractor>(serviceProvider =>
{
var extractor = ActivatorUtilities.CreateInstance(serviceProvider, type);
var decorator = ActivatorUtilities.CreateInstance<CacheDecorator>(serviceProvider, extractor);
return decorator;
});
}
}

View File

@ -0,0 +1,58 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Emby.Naming.Common;
using Jellyfin.Extensions;
using Jellyfin.MediaEncoding.Keyframes;
using MediaBrowser.Controller.MediaEncoding;
using Microsoft.Extensions.Logging;
using Extractor = Jellyfin.MediaEncoding.Keyframes.FfProbe.FfProbeKeyframeExtractor;
namespace Jellyfin.MediaEncoding.Hls.Extractors;
/// <inheritdoc />
public class FfProbeKeyframeExtractor : IKeyframeExtractor
{
private readonly IMediaEncoder _mediaEncoder;
private readonly NamingOptions _namingOptions;
private readonly ILogger<FfProbeKeyframeExtractor> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="FfProbeKeyframeExtractor"/> class.
/// </summary>
/// <param name="mediaEncoder">An instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</param>
/// <param name="logger">An instance of the <see cref="ILogger{FfprobeKeyframeExtractor}"/> interface.</param>
public FfProbeKeyframeExtractor(IMediaEncoder mediaEncoder, NamingOptions namingOptions, ILogger<FfProbeKeyframeExtractor> logger)
{
_mediaEncoder = mediaEncoder;
_namingOptions = namingOptions;
_logger = logger;
}
/// <inheritdoc />
public bool IsMetadataBased => false;
/// <inheritdoc />
public bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
{
if (!_namingOptions.VideoFileExtensions.Contains(Path.GetExtension(filePath.AsSpan()), StringComparison.OrdinalIgnoreCase))
{
keyframeData = null;
return false;
}
try
{
keyframeData = Extractor.GetKeyframeData(_mediaEncoder.ProbePath, filePath);
return keyframeData.KeyframeTicks.Count > 0;
}
catch (Exception ex)
{
_logger.LogError(ex, "Extracting keyframes from {FilePath} using ffprobe failed", filePath);
}
keyframeData = null;
return false;
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Jellyfin.MediaEncoding.Keyframes;
namespace Jellyfin.MediaEncoding.Hls.Extractors;
/// <summary>
/// Keyframe extractor.
/// </summary>
public interface IKeyframeExtractor
{
/// <summary>
/// Gets a value indicating whether the extractor is based on container metadata.
/// </summary>
bool IsMetadataBased { get; }
/// <summary>
/// Attempt to extract keyframes.
/// </summary>
/// <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);
}

View File

@ -0,0 +1,48 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Jellyfin.MediaEncoding.Keyframes;
using Microsoft.Extensions.Logging;
using Extractor = Jellyfin.MediaEncoding.Keyframes.Matroska.MatroskaKeyframeExtractor;
namespace Jellyfin.MediaEncoding.Hls.Extractors;
/// <inheritdoc />
public class MatroskaKeyframeExtractor : IKeyframeExtractor
{
private readonly ILogger<MatroskaKeyframeExtractor> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="MatroskaKeyframeExtractor"/> class.
/// </summary>
/// <param name="logger">An instance of the <see cref="ILogger{MatroskaKeyframeExtractor}"/> interface.</param>
public MatroskaKeyframeExtractor(ILogger<MatroskaKeyframeExtractor> logger)
{
_logger = logger;
}
/// <inheritdoc />
public bool IsMetadataBased => true;
/// <inheritdoc />
public bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
{
if (filePath.AsSpan().EndsWith(".mkv", StringComparison.OrdinalIgnoreCase))
{
keyframeData = null;
return false;
}
try
{
keyframeData = Extractor.GetKeyframeData(filePath);
return keyframeData.KeyframeTicks.Count > 0;
}
catch (Exception ex)
{
_logger.LogError(ex, "Extracting keyframes from {FilePath} using matroska metadata failed", filePath);
}
keyframeData = null;
return false;
}
}

View File

@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
</ItemGroup>
<ItemGroup>

View File

@ -1,5 +1,5 @@
namespace Jellyfin.MediaEncoding.Hls.Playlist
{
namespace Jellyfin.MediaEncoding.Hls.Playlist;
/// <summary>
/// Request class for the <see cref="IDynamicHlsPlaylistGenerator.CreateMainPlaylist(CreateMainPlaylistRequest)"/> method.
/// </summary>
@ -54,4 +54,3 @@
/// </summary>
public string QueryString { get; }
}
}

View File

@ -5,47 +5,31 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using Jellyfin.Extensions.Json;
using Jellyfin.MediaEncoding.Hls.Extractors;
using Jellyfin.MediaEncoding.Keyframes;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.MediaEncoding;
using Microsoft.Extensions.Logging;
namespace Jellyfin.MediaEncoding.Hls.Playlist
{
namespace Jellyfin.MediaEncoding.Hls.Playlist;
/// <inheritdoc />
public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator
{
private const string DefaultContainerExtension = ".ts";
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IApplicationPaths _applicationPaths;
private readonly KeyframeExtractor _keyframeExtractor;
private readonly ILogger<DynamicHlsPlaylistGenerator> _logger;
private readonly IKeyframeExtractor[] _extractors;
/// <summary>
/// Initializes a new instance of the <see cref="DynamicHlsPlaylistGenerator"/> class.
/// </summary>
/// <param name="serverConfigurationManager">An instance of the see <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">An instance of the see <see cref="IMediaEncoder"/> interface.</param>
/// <param name="applicationPaths">An instance of the <see cref="IApplicationPaths"/> interface.</param>
/// <param name="loggerFactory">An instance of the see <see cref="ILoggerFactory"/> interface.</param>
public DynamicHlsPlaylistGenerator(IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, IApplicationPaths applicationPaths, ILoggerFactory loggerFactory)
/// <param name="extractors">An instance of <see cref="IEnumerable{IKeyframeExtractor}"/>.</param>
public DynamicHlsPlaylistGenerator(IServerConfigurationManager serverConfigurationManager, IEnumerable<IKeyframeExtractor> extractors)
{
_serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder;
_applicationPaths = applicationPaths;
_keyframeExtractor = new KeyframeExtractor(loggerFactory.CreateLogger<KeyframeExtractor>());
_logger = loggerFactory.CreateLogger<DynamicHlsPlaylistGenerator>();
_extractors = extractors.Where(e => e.IsMetadataBased).ToArray();
}
private string KeyframeCachePath => Path.Combine(_applicationPaths.DataPath, "keyframes");
/// <inheritdoc />
public string CreateMainPlaylist(CreateMainPlaylistRequest request)
{
@ -59,7 +43,7 @@ namespace Jellyfin.MediaEncoding.Hls.Playlist
segments = ComputeEqualLengthSegments(request.DesiredSegmentLengthMs, request.TotalRuntimeTicks);
}
var segmentExtension = GetSegmentFileExtension(request.SegmentContainer);
var segmentExtension = EncodingHelper.GetSegmentFileExtension(request.SegmentContainer);
// http://ffmpeg.org/ffmpeg-all.html#toc-hls-2
var isHlsInFmp4 = string.Equals(segmentExtension, "mp4", StringComparison.OrdinalIgnoreCase);
@ -119,65 +103,24 @@ namespace Jellyfin.MediaEncoding.Hls.Playlist
private bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
{
keyframeData = null;
if (!IsExtractionAllowedForFile(filePath, _serverConfigurationManager.GetEncodingOptions().AllowAutomaticKeyframeExtractionForExtensions))
if (!IsExtractionAllowedForFile(filePath, _serverConfigurationManager.GetEncodingOptions().AllowOnDemandMetadataBasedKeyframeExtractionForExtensions))
{
return false;
}
var succeeded = false;
var cachePath = GetCachePath(filePath);
if (TryReadFromCache(cachePath, out var cachedResult))
var len = _extractors.Length;
for (var i = 0; i < len; i++)
{
keyframeData = cachedResult;
}
else
var extractor = _extractors[i];
if (!extractor.TryExtractKeyframes(filePath, out var result))
{
try
{
keyframeData = _keyframeExtractor.GetKeyframeData(filePath, _mediaEncoder.ProbePath, string.Empty);
}
catch (Exception ex)
{
_logger.LogError(ex, "Keyframe extraction failed for path {FilePath}", filePath);
return false;
continue;
}
succeeded = keyframeData.KeyframeTicks.Count > 0;
if (succeeded)
{
CacheResult(cachePath, keyframeData);
}
keyframeData = result;
return true;
}
return succeeded;
}
private void CacheResult(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 string GetCachePath(string filePath)
{
var lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath);
ReadOnlySpan<char> filename = (filePath + "_" + lastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5() + ".json";
var prefix = filename.Slice(0, 1);
return Path.Join(KeyframeCachePath, prefix, filename);
}
private 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 != null;
}
cachedResult = null;
return false;
}
@ -193,7 +136,7 @@ namespace Jellyfin.MediaEncoding.Hls.Playlist
var extensionWithoutDot = extension[1..];
for (var i = 0; i < allowedExtensions.Length; i++)
{
var allowedExtension = allowedExtensions[i];
var allowedExtension = allowedExtensions[i].AsSpan().TrimStart('.');
if (extensionWithoutDot.Equals(allowedExtension, StringComparison.OrdinalIgnoreCase))
{
return true;
@ -258,16 +201,4 @@ namespace Jellyfin.MediaEncoding.Hls.Playlist
return segments;
}
// TODO copied from DynamicHlsController
private static string GetSegmentFileExtension(string segmentContainer)
{
if (!string.IsNullOrWhiteSpace(segmentContainer))
{
return "." + segmentContainer;
}
return DefaultContainerExtension;
}
}
}

View File

@ -1,5 +1,5 @@
namespace Jellyfin.MediaEncoding.Hls.Playlist
{
namespace Jellyfin.MediaEncoding.Hls.Playlist;
/// <summary>
/// Generator for dynamic HLS playlists where the segment lengths aren't known in advance.
/// </summary>
@ -12,4 +12,3 @@
/// <returns>The playlist as a formatted string.</returns>
string CreateMainPlaylist(CreateMainPlaylistRequest request);
}
}

View File

@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
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;
namespace Jellyfin.MediaEncoding.Hls.ScheduledTasks;
/// <inheritdoc />
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 };
/// <summary>
/// Initializes a new instance of the <see cref="KeyframeExtractionScheduledTask"/> class.
/// </summary>
/// <param name="localizationManager">An instance of the <see cref="ILocalizationManager"/> interface.</param>
/// <param name="libraryManager">An instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="keyframeExtractors">The keyframe extractors.</param>
public KeyframeExtractionScheduledTask(ILocalizationManager localizationManager, ILibraryManager libraryManager, IEnumerable<IKeyframeExtractor> keyframeExtractors)
{
_localizationManager = localizationManager;
_libraryManager = libraryManager;
_keyframeExtractors = keyframeExtractors.ToArray();
}
/// <inheritdoc />
public string Name => "Keyframe Extractor";
/// <inheritdoc />
public string Key => "KeyframeExtraction";
/// <inheritdoc />
public string Description => "Extracts keyframes from video files to create more precise HLS playlists";
/// <inheritdoc />
public string Category => _localizationManager.GetLocalizedString("TasksLibraryCategory");
/// <inheritdoc />
public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
{
var query = new InternalItemsQuery
{
MediaTypes = new[] { MediaType.Video },
IsVirtualItem = false,
IncludeItemTypes = _itemTypes,
DtoOptions = new DtoOptions(true),
SourceTypes = new[] { SourceType.Library },
Recursive = true
};
var videos = _libraryManager.GetItemList(query);
// TODO parallelize with Parallel.ForEach?
for (var i = 0; i < videos.Count; i++)
{
var video = videos[i];
// Only local files supported
if (!video.IsFileProtocol || !File.Exists(video.Path))
{
continue;
}
for (var j = 0; j < _keyframeExtractors.Length; j++)
{
var extractor = _keyframeExtractors[j];
// The cache decorator will make sure to save them in the data dir
if (extractor.TryExtractKeyframes(video.Path, out _))
{
break;
}
}
}
return Task.CompletedTask;
}
/// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => Enumerable.Empty<TaskTriggerInfo>();
}

View File

@ -4,8 +4,8 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
namespace Jellyfin.MediaEncoding.Keyframes.FfProbe
{
namespace Jellyfin.MediaEncoding.Keyframes.FfProbe;
/// <summary>
/// FfProbe based keyframe extractor.
/// </summary>
@ -92,4 +92,3 @@ namespace Jellyfin.MediaEncoding.Keyframes.FfProbe
return new KeyframeData(TimeSpan.FromSeconds(duration).Ticks, keyframes);
}
}
}

View File

@ -1,7 +1,7 @@
using System;
namespace Jellyfin.MediaEncoding.Keyframes.FfTool
{
namespace Jellyfin.MediaEncoding.Keyframes.FfTool;
/// <summary>
/// FfTool based keyframe extractor.
/// </summary>
@ -15,4 +15,3 @@ namespace Jellyfin.MediaEncoding.Keyframes.FfTool
/// <returns>An instance of <see cref="KeyframeData"/>.</returns>
public static KeyframeData GetKeyframeData(string ffToolPath, string filePath) => throw new NotImplementedException();
}
}

View File

@ -1,7 +1,7 @@
using System.Collections.Generic;
namespace Jellyfin.MediaEncoding.Keyframes
{
namespace Jellyfin.MediaEncoding.Keyframes;
/// <summary>
/// Keyframe information for a specific file.
/// </summary>
@ -28,4 +28,3 @@ namespace Jellyfin.MediaEncoding.Keyframes
/// </summary>
public IReadOnlyList<long> KeyframeTicks { get; }
}
}

View File

@ -1,69 +0,0 @@
using System;
using System.IO;
using Jellyfin.MediaEncoding.Keyframes.FfProbe;
using Jellyfin.MediaEncoding.Keyframes.FfTool;
using Jellyfin.MediaEncoding.Keyframes.Matroska;
using Microsoft.Extensions.Logging;
namespace Jellyfin.MediaEncoding.Keyframes
{
/// <summary>
/// Manager class for the set of keyframe extractors.
/// </summary>
public class KeyframeExtractor
{
private readonly ILogger<KeyframeExtractor> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="KeyframeExtractor"/> class.
/// </summary>
/// <param name="logger">An instance of the <see cref="ILogger{KeyframeExtractor}"/> interface.</param>
public KeyframeExtractor(ILogger<KeyframeExtractor> logger)
{
_logger = logger;
}
/// <summary>
/// Extracts the keyframe positions from a video file.
/// </summary>
/// <param name="filePath">Absolute file path to the media file.</param>
/// <param name="ffProbePath">Absolute file path to the ffprobe executable.</param>
/// <param name="ffToolPath">Absolute file path to the fftool executable.</param>
/// <returns>An instance of <see cref="KeyframeData"/>.</returns>
public KeyframeData GetKeyframeData(string filePath, string ffProbePath, string ffToolPath)
{
var extension = Path.GetExtension(filePath.AsSpan());
if (extension.Equals(".mkv", StringComparison.OrdinalIgnoreCase))
{
try
{
return MatroskaKeyframeExtractor.GetKeyframeData(filePath);
}
catch (Exception ex)
{
_logger.LogError(ex, "{ExtractorType} failed to extract keyframes", nameof(MatroskaKeyframeExtractor));
}
}
try
{
return FfToolKeyframeExtractor.GetKeyframeData(ffToolPath, filePath);
}
catch (Exception ex)
{
_logger.LogError(ex, "{ExtractorType} failed to extract keyframes", nameof(FfToolKeyframeExtractor));
}
try
{
return FfProbeKeyframeExtractor.GetKeyframeData(ffProbePath, filePath);
}
catch (Exception ex)
{
_logger.LogError(ex, "{ExtractorType} failed to extract keyframes", nameof(FfProbeKeyframeExtractor));
}
return new KeyframeData(0, Array.Empty<long>());
}
}
}

View File

@ -3,8 +3,8 @@ using System.Buffers.Binary;
using Jellyfin.MediaEncoding.Keyframes.Matroska.Models;
using NEbml.Core;
namespace Jellyfin.MediaEncoding.Keyframes.Matroska.Extensions
{
namespace Jellyfin.MediaEncoding.Keyframes.Matroska.Extensions;
/// <summary>
/// Extension methods for the <see cref="EbmlReader"/> class.
/// </summary>
@ -175,4 +175,3 @@ namespace Jellyfin.MediaEncoding.Keyframes.Matroska.Extensions
throw new InvalidOperationException($"No stream with type {type} found");
}
}
}

View File

@ -1,5 +1,5 @@
namespace Jellyfin.MediaEncoding.Keyframes.Matroska
{
namespace Jellyfin.MediaEncoding.Keyframes.Matroska;
/// <summary>
/// Constants for the Matroska identifiers.
/// </summary>
@ -28,4 +28,3 @@
internal const ulong CueTrackPositions = 0xB7;
internal const ulong CuePointTrackNumber = 0xF7;
}
}

View File

@ -4,8 +4,8 @@ using System.IO;
using Jellyfin.MediaEncoding.Keyframes.Matroska.Extensions;
using NEbml.Core;
namespace Jellyfin.MediaEncoding.Keyframes.Matroska
{
namespace Jellyfin.MediaEncoding.Keyframes.Matroska;
/// <summary>
/// The keyframe extractor for the matroska container.
/// </summary>
@ -73,4 +73,3 @@ namespace Jellyfin.MediaEncoding.Keyframes.Matroska
return Convert.ToInt64(unscaledValue * timestampScale / 100);
}
}
}

View File

@ -1,5 +1,5 @@
namespace Jellyfin.MediaEncoding.Keyframes.Matroska.Models
{
namespace Jellyfin.MediaEncoding.Keyframes.Matroska.Models;
/// <summary>
/// The matroska Info segment.
/// </summary>
@ -26,4 +26,3 @@
/// </summary>
public double? Duration { get; }
}
}

View File

@ -1,5 +1,5 @@
namespace Jellyfin.MediaEncoding.Keyframes.Matroska.Models
{
namespace Jellyfin.MediaEncoding.Keyframes.Matroska.Models;
/// <summary>
/// The matroska SeekHead segment. All positions are relative to the Segment container.
/// </summary>
@ -33,4 +33,3 @@
/// </summary>
public long CuesPosition { get; }
}
}

View File

@ -38,8 +38,8 @@ namespace Jellyfin.MediaEncoding.Hls.Tests.Playlist
[Theory]
[InlineData("testfile.mkv", new string[0], false)]
[InlineData("testfile.flv", new[] { "mp4", "mkv", "ts" }, false)]
[InlineData("testfile.flv", new[] { "mp4", "mkv", "ts", "flv" }, true)]
[InlineData("testfile.flv", new[] { ".mp4", ".mkv", ".ts" }, false)]
[InlineData("testfile.flv", new[] { ".mp4", ".mkv", ".ts", ".flv" }, true)]
[InlineData("/some/arbitrarily/long/path/testfile.mkv", new[] { "mkv" }, true)]
public void IsExtractionAllowedForFile_Valid_Success(string filePath, string[] allowedExtensions, bool isAllowed)
{
@ -47,7 +47,7 @@ namespace Jellyfin.MediaEncoding.Hls.Tests.Playlist
}
[Theory]
[InlineData("testfile", new[] { "mp4" })]
[InlineData("testfile", new[] { ".mp4" })]
public void IsExtractionAllowedForFile_Invalid_ReturnsFalse(string filePath, string[] allowedExtensions)
{
Assert.False(DynamicHlsPlaylistGenerator.IsExtractionAllowedForFile(filePath, allowedExtensions));

View File

@ -13,7 +13,7 @@ namespace Jellyfin.MediaEncoding.Keyframes.FfProbe
{
var testDataPath = Path.Combine("FfProbe/Test Data", testDataFileName);
var resultPath = Path.Combine("FfProbe/Test Data", resultFileName);
var resultFileStream = File.OpenRead(resultPath);
using var resultFileStream = File.OpenRead(resultPath);
var expectedResult = JsonSerializer.Deserialize<KeyframeData>(resultFileStream)!;
using var fileStream = File.OpenRead(testDataPath);