Implement FfProbeKeyframeExtractor and add tests for it

pull/6600/head
cvium 3 years ago
parent 41383e6fe4
commit 2899b77cd5

@ -95,6 +95,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.MediaEncoding.Hls"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.MediaEncoding.Hls.Tests", "tests\Jellyfin.MediaEncoding.Hls.Tests\Jellyfin.MediaEncoding.Hls.Tests.csproj", "{FE47334C-EFDE-4519-BD50-F24430FF360B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.MediaEncoding.Keyframes.Tests", "tests\Jellyfin.MediaEncoding.Keyframes.Tests\Jellyfin.MediaEncoding.Keyframes.Tests.csproj", "{24960660-DE6C-47BF-AEEF-CEE8F19FE6C2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -257,6 +259,10 @@ Global
{FE47334C-EFDE-4519-BD50-F24430FF360B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FE47334C-EFDE-4519-BD50-F24430FF360B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FE47334C-EFDE-4519-BD50-F24430FF360B}.Release|Any CPU.Build.0 = Release|Any CPU
{24960660-DE6C-47BF-AEEF-CEE8F19FE6C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{24960660-DE6C-47BF-AEEF-CEE8F19FE6C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{24960660-DE6C-47BF-AEEF-CEE8F19FE6C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{24960660-DE6C-47BF-AEEF-CEE8F19FE6C2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -280,6 +286,7 @@ Global
{06535CA1-4097-4360-85EB-5FB875D53239} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
{DA9FD356-4894-4830-B208-D6BCE3E65B11} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C}
{FE47334C-EFDE-4519-BD50-F24430FF360B} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{24960660-DE6C-47BF-AEEF-CEE8F19FE6C2} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}

@ -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)

@ -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);
}
}
}

@ -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>

@ -27,6 +27,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" />
<ProjectReference Include="..\..\src\Jellyfin.MediaEncoding.Keyframes\Jellyfin.MediaEncoding.Keyframes.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,28 @@
using System.IO;
using System.Text.Json;
using Xunit;
namespace Jellyfin.MediaEncoding.Keyframes.FfProbe
{
public class FfProbeKeyframeExtractorTests
{
[Theory]
[InlineData("keyframes.txt", "keyframes_result.json")]
[InlineData("keyframes_streamduration.txt", "keyframes_streamduration_result.json")]
public void ParseStream_Valid_Success(string testDataFileName, string resultFileName)
{
var testDataPath = Path.Combine("FfProbe/Test Data", testDataFileName);
var resultPath = Path.Combine("FfProbe/Test Data", resultFileName);
var resultFileStream = File.OpenRead(resultPath);
var expectedResult = JsonSerializer.Deserialize<KeyframeData>(resultFileStream)!;
using var fileStream = File.OpenRead(testDataPath);
using var streamReader = new StreamReader(fileStream);
var result = FfProbeKeyframeExtractor.ParseStream(streamReader);
Assert.Equal(expectedResult.TotalDuration, result.TotalDuration);
Assert.Equal(expectedResult.KeyframeTicks, result.KeyframeTicks);
}
}
}

@ -0,0 +1 @@
{"TotalDuration":7063360000,"KeyframeTicks":[0,103850000,133880000,145150000,165580000,186440000,196450000,209790000,314060000,326990000,396230000,407070000,432520000,476310000,523020000,535540000,550550000,631050000,646480000,665670000,686520000,732400000,772020000,796210000,856690000,887970000,903820000,934270000,983070000,1056060000,1087750000,1187850000,1222050000,1251250000,1265430000,1305470000,1333830000,1345510000,1356770000,1368450000,1427260000,1460630000,1500670000,1540710000,1584500000,1607020000,1627880000,1639550000,1672090000,1685020000,1789290000,1883130000,1909820000,1931510000,1996580000,2017020000,2035370000,2051220000,2065400000,2085000000,2109190000,2120870000,2168420000,2253920000,2295210000,2374460000,2478730000,2582160000,2607190000,2697280000,2783610000,2825320000,2899560000,2929590000,2979230000,3017600000,3048880000,3073490000,3117700000,3141050000,3158160000,3200700000,3279530000,3299960000,3312890000,3332910000,3369200000,3379630000,3438440000,3459290000,3490990000,3533110000,3562730000,3600260000,3624040000,3672000000,3722050000,3753330000,3771270000,3875540000,3957290000,4016100000,4100350000,4114530000,4124540000,4157900000,4180430000,4200450000,4222550000,4252160000,4295960000,4309720000,4328070000,4340590000,4371450000,4400230000,4426920000,4489490000,4512010000,4531190000,4569570000,4599600000,4635460000,4660070000,4680930000,4729310000,4757670000,4777690000,4808550000,4824400000,4851100000,4864440000,4905320000,4955370000,4970380000,5074650000,5095090000,5109270000,5186010000,5204370000,5227720000,5242740000,5266930000,5342000000,5433760000,5447110000,5470470000,5520520000,5550550000,5565140000,5611020000,5642300000,5668160000,5711120000,5743240000,5762420000,5797460000,5817480000,5839170000,5855850000,5870870000,5904230000,5969300000,6056880000,6104850000,6152400000,6256250000,6295870000,6310050000,6325900000,6341750000,6356770000,6385960000,6426840000,6454780000,6469800000,6514420000,6549460000,6574480000,6602010000,6619530000,6654560000,6667080000,6690430000,6724630000,6762170000,6812220000,6849760000,6875200000,6912740000,6983230000,6994900000,7024930000]}

@ -0,0 +1 @@
{"TotalDuration":1000000000,"KeyframeTicks":[0,103850000,133880000,145150000,165580000,186440000,196450000,209790000,314060000,326990000,396230000,407070000,432520000,476310000,523020000,535540000,550550000,631050000,646480000,665670000,686520000,732400000,772020000,796210000,856690000,887970000,903820000,934270000,983070000]}

@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<CodeAnalysisRuleSet>../jellyfin-tests.ruleset</CodeAnalysisRuleSet>
<RootNamespace>Jellyfin.MediaEncoding.Keyframes</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="FfProbe/Test Data/keyframes.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="FfProbe/Test Data/keyframes_result.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="FfProbe/Test Data/keyframes_streamduration.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="FfProbe/Test Data/keyframes_streamduration_result.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
Loading…
Cancel
Save