diff --git a/Jellyfin.sln b/Jellyfin.sln index 68570a214..436df9a58 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -223,10 +223,6 @@ Global {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.Build.0 = Debug|Any CPU {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.ActiveCfg = Release|Any CPU {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.Build.0 = Release|Any CPU - {25E40B0B-7C89-4230-8911-CBBBCE83FC5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {25E40B0B-7C89-4230-8911-CBBBCE83FC5B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {25E40B0B-7C89-4230-8911-CBBBCE83FC5B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {25E40B0B-7C89-4230-8911-CBBBCE83FC5B}.Release|Any CPU.Build.0 = Release|Any CPU {3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU {3ADBCD8C-C0F2-4956-8FDC-35D686B74CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -249,12 +245,12 @@ Global {332A5C7A-F907-47CA-910E-BE6F7371B9E0}.Release|Any CPU.Build.0 = Release|Any CPU {06535CA1-4097-4360-85EB-5FB875D53239}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {06535CA1-4097-4360-85EB-5FB875D53239}.Debug|Any CPU.Build.0 = Debug|Any CPU - {06535CA1-4097-4360-85EB-5FB875D53239}.Release|Any CPU.ActiveCfg = Release|Any CPU - {06535CA1-4097-4360-85EB-5FB875D53239}.Release|Any CPU.Build.0 = Release|Any CPU + {06535CA1-4097-4360-85EB-5FB875D53239}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {06535CA1-4097-4360-85EB-5FB875D53239}.Release|Any CPU.Build.0 = Debug|Any CPU {DA9FD356-4894-4830-B208-D6BCE3E65B11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DA9FD356-4894-4830-B208-D6BCE3E65B11}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DA9FD356-4894-4830-B208-D6BCE3E65B11}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DA9FD356-4894-4830-B208-D6BCE3E65B11}.Release|Any CPU.Build.0 = Release|Any CPU + {DA9FD356-4894-4830-B208-D6BCE3E65B11}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {DA9FD356-4894-4830-B208-D6BCE3E65B11}.Release|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs b/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs index 0395d7ea4..13204c757 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs +++ b/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs @@ -26,6 +26,7 @@ namespace Jellyfin.MediaEncoding.Hls.Playlist private readonly IMediaEncoder _mediaEncoder; private readonly IApplicationPaths _applicationPaths; private readonly KeyframeExtractor _keyframeExtractor; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. @@ -40,6 +41,7 @@ namespace Jellyfin.MediaEncoding.Hls.Playlist _mediaEncoder = mediaEncoder; _applicationPaths = applicationPaths; _keyframeExtractor = new KeyframeExtractor(loggerFactory.CreateLogger()); + _logger = loggerFactory.CreateLogger(); } private string KeyframeCachePath => Path.Combine(_applicationPaths.DataPath, "keyframes"); @@ -47,12 +49,13 @@ namespace Jellyfin.MediaEncoding.Hls.Playlist /// public string CreateMainPlaylist(CreateMainPlaylistRequest request) { - IReadOnlyList segments; - if (IsExtractionAllowed(request.FilePath)) + IReadOnlyList segments = Array.Empty(); + if (IsExtractionAllowedForFile(request.FilePath)) { segments = ComputeSegments(request.FilePath, request.DesiredSegmentLengthMs); } - else + + if (segments.Count == 0) { segments = ComputeEqualLengthSegments(request.DesiredSegmentLengthMs, request.TotalRuntimeTicks); } @@ -92,7 +95,7 @@ namespace Jellyfin.MediaEncoding.Hls.Playlist foreach (var length in segments) { builder.Append("#EXTINF:") - .Append(length.ToString("0.0000", CultureInfo.InvariantCulture)) + .Append(length.ToString("0.000000", CultureInfo.InvariantCulture)) .AppendLine(", nodesc") .Append(request.EndpointPrefix) .Append(index++) @@ -122,7 +125,16 @@ namespace Jellyfin.MediaEncoding.Hls.Playlist } else { - keyframeData = _keyframeExtractor.GetKeyframeData(filePath, _mediaEncoder.ProbePath, string.Empty); + try + { + keyframeData = _keyframeExtractor.GetKeyframeData(filePath, _mediaEncoder.ProbePath, string.Empty); + } + catch (Exception ex) + { + _logger.LogError(ex, "Keyframe extraction failed for path {FilePath}", filePath); + return Array.Empty(); + } + CacheResult(cachePath, keyframeData); } @@ -176,7 +188,7 @@ namespace Jellyfin.MediaEncoding.Hls.Playlist return false; } - private bool IsExtractionAllowed(ReadOnlySpan filePath) + private bool IsExtractionAllowedForFile(ReadOnlySpan filePath) { // Remove the leading dot var extension = Path.GetExtension(filePath)[1..]; diff --git a/src/Jellyfin.MediaEncoding.Keyframes/KeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Keyframes/KeyframeExtractor.cs index 641998273..5304a55f8 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/KeyframeExtractor.cs +++ b/src/Jellyfin.MediaEncoding.Keyframes/KeyframeExtractor.cs @@ -32,25 +32,38 @@ namespace Jellyfin.MediaEncoding.Keyframes /// An instance of . public KeyframeData GetKeyframeData(string filePath, string ffProbePath, string ffToolPath) { - var extension = Path.GetExtension(filePath); - if (string.Equals(extension, ".mkv", StringComparison.OrdinalIgnoreCase)) + var extension = Path.GetExtension(filePath.AsSpan()); + if (extension.Equals(".mkv", StringComparison.OrdinalIgnoreCase)) { try { return MatroskaKeyframeExtractor.GetKeyframeData(filePath); } - catch (InvalidOperationException ex) + catch (Exception ex) { - _logger.LogError(ex, "{MatroskaKeyframeExtractor} failed to extract keyframes", nameof(MatroskaKeyframeExtractor)); + _logger.LogError(ex, "{ExtractorType} failed to extract keyframes", nameof(MatroskaKeyframeExtractor)); } } - if (!string.IsNullOrEmpty(ffToolPath)) + try { return FfToolKeyframeExtractor.GetKeyframeData(ffToolPath, filePath); } + catch (Exception ex) + { + _logger.LogError(ex, "{ExtractorType} failed to extract keyframes", nameof(FfToolKeyframeExtractor)); + } - return FfProbeKeyframeExtractor.GetKeyframeData(ffProbePath, filePath); + 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()); } } } diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Matroska/MatroskaKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Keyframes/Matroska/MatroskaKeyframeExtractor.cs index 10d017d2a..6a8a55643 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/Matroska/MatroskaKeyframeExtractor.cs +++ b/src/Jellyfin.MediaEncoding.Keyframes/Matroska/MatroskaKeyframeExtractor.cs @@ -49,7 +49,7 @@ namespace Jellyfin.MediaEncoding.Keyframes.Matroska if (trackNumber == videoTrackNumber) { - keyframes.Add(ScaleToNanoseconds(cueTime, info.TimestampScale)); + keyframes.Add(ScaleToTicks(cueTime, info.TimestampScale)); } reader.LeaveContainer(); @@ -57,17 +57,17 @@ namespace Jellyfin.MediaEncoding.Keyframes.Matroska reader.LeaveContainer(); - var result = new KeyframeData(ScaleToNanoseconds(info.Duration ?? 0, info.TimestampScale), keyframes); + var result = new KeyframeData(ScaleToTicks(info.Duration ?? 0, info.TimestampScale), keyframes); return result; } - private static long ScaleToNanoseconds(ulong unscaledValue, long timestampScale) + private static long ScaleToTicks(ulong unscaledValue, long timestampScale) { // TimestampScale is in nanoseconds, scale it to get the value in ticks, 1 tick == 100 ns return (long)unscaledValue * timestampScale / 100; } - private static long ScaleToNanoseconds(double unscaledValue, long timestampScale) + private static long ScaleToTicks(double unscaledValue, long timestampScale) { // TimestampScale is in nanoseconds, scale it to get the value in ticks, 1 tick == 100 ns return Convert.ToInt64(unscaledValue * timestampScale / 100);