diff options
| author | cvium <clausvium@gmail.com> | 2021-09-26 21:07:30 +0200 |
|---|---|---|
| committer | cvium <clausvium@gmail.com> | 2021-09-26 21:07:30 +0200 |
| commit | 2899b77cd58456470b8dd4d01d3a8c525a9b5911 (patch) | |
| tree | decd61105f437fb0c0efc75b02d433b1aca29407 /src | |
| parent | 41383e6fe4d2ae80552c1b3df0b469f5416f2d89 (diff) | |
Implement FfProbeKeyframeExtractor and add tests for it
Diffstat (limited to 'src')
3 files changed, 97 insertions, 7 deletions
diff --git a/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs b/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs index 5411d5667..cbc62eb59 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs +++ b/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs @@ -90,9 +90,11 @@ namespace Jellyfin.MediaEncoding.Hls.Playlist .AppendLine(); } - double currentRuntimeInSeconds = 0; + long currentRuntimeInSeconds = 0; foreach (var length in segments) { + // Manually convert to ticks to avoid precision loss when converting double + var lengthTicks = Convert.ToInt64(length * TimeSpan.TicksPerSecond); builder.Append("#EXTINF:") .Append(length.ToString("0.000000", CultureInfo.InvariantCulture)) .AppendLine(", nodesc") @@ -101,12 +103,12 @@ namespace Jellyfin.MediaEncoding.Hls.Playlist .Append(segmentExtension) .Append(request.QueryString) .Append("&runtimeTicks=") - .Append(TimeSpan.FromSeconds(currentRuntimeInSeconds).Ticks) + .Append(currentRuntimeInSeconds) .Append("&actualSegmentLengthTicks=") - .Append(TimeSpan.FromSeconds(length).Ticks) + .Append(lengthTicks) .AppendLine(); - currentRuntimeInSeconds += length; + currentRuntimeInSeconds += lengthTicks; } builder.AppendLine("#EXT-X-ENDLIST"); @@ -122,6 +124,7 @@ namespace Jellyfin.MediaEncoding.Hls.Playlist return false; } + var succeeded = false; var cachePath = GetCachePath(filePath); if (TryReadFromCache(cachePath, out var cachedResult)) { @@ -139,10 +142,14 @@ namespace Jellyfin.MediaEncoding.Hls.Playlist return false; } - CacheResult(cachePath, keyframeData); + succeeded = keyframeData.KeyframeTicks.Count > 0; + if (succeeded) + { + CacheResult(cachePath, keyframeData); + } } - return keyframeData.KeyframeTicks.Count > 0; + return succeeded; } private void CacheResult(string cachePath, KeyframeData keyframeData) diff --git a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs index e09b9da9a..351d880fe 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs +++ b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs @@ -1,4 +1,8 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; namespace Jellyfin.MediaEncoding.Keyframes.FfProbe { @@ -7,12 +11,85 @@ namespace Jellyfin.MediaEncoding.Keyframes.FfProbe /// </summary> public static class FfProbeKeyframeExtractor { + private const string DefaultArguments = "-v error -skip_frame nokey -show_entries format=duration -show_entries stream=duration -show_entries packet=pts_time,flags -select_streams v -of csv \"{0}\""; + /// <summary> /// Extracts the keyframes using the ffprobe executable at the specified path. /// </summary> /// <param name="ffProbePath">The path to the ffprobe executable.</param> /// <param name="filePath">The file path.</param> /// <returns>An instance of <see cref="KeyframeData"/>.</returns> - public static KeyframeData GetKeyframeData(string ffProbePath, string filePath) => throw new NotImplementedException(); + public static KeyframeData GetKeyframeData(string ffProbePath, string filePath) + { + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = ffProbePath, + Arguments = string.Format(CultureInfo.InvariantCulture, DefaultArguments, filePath), + + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardOutput = true, + + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false, + }, + EnableRaisingEvents = true + }; + + process.Start(); + + return ParseStream(process.StandardOutput); + } + + internal static KeyframeData ParseStream(StreamReader reader) + { + var keyframes = new List<long>(); + double streamDuration = 0; + double formatDuration = 0; + + while (!reader.EndOfStream) + { + var line = reader.ReadLine().AsSpan(); + if (line.IsEmpty) + { + continue; + } + + var firstComma = line.IndexOf(','); + var lineType = line[..firstComma]; + var rest = line[(firstComma + 1)..]; + if (lineType.Equals("packet", StringComparison.OrdinalIgnoreCase)) + { + if (rest.EndsWith(",K_")) + { + // Trim the flags from the packet line. Example line: packet,7169.079000,K_ + var keyframe = double.Parse(rest[..^3], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture); + // Have to manually convert to ticks to avoid rounding errors as TimeSpan is only precise down to 1 ms when converting double. + keyframes.Add(Convert.ToInt64(keyframe * TimeSpan.TicksPerSecond)); + } + } + else if (lineType.Equals("stream", StringComparison.OrdinalIgnoreCase)) + { + if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var streamDurationResult)) + { + streamDuration = streamDurationResult; + } + } + else if (lineType.Equals("format", StringComparison.OrdinalIgnoreCase)) + { + if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var formatDurationResult)) + { + formatDuration = formatDurationResult; + } + } + } + + // Prefer the stream duration as it should be more accurate + var duration = streamDuration > 0 ? streamDuration : formatDuration; + + return new KeyframeData(TimeSpan.FromSeconds(duration).Ticks, keyframes); + } } } diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj index d3eb44d2b..22db10d72 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj +++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj @@ -20,4 +20,10 @@ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" /> </ItemGroup> + <ItemGroup> + <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo"> + <_Parameter1>Jellyfin.MediaEncoding.Keyframes.Tests</_Parameter1> + </AssemblyAttribute> + </ItemGroup> + </Project> |
