Implement FfProbeKeyframeExtractor and add tests for it
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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]}
|
File diff suppressed because it is too large
Load Diff
|
@ -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…
Reference in New Issue
Block a user