aboutsummaryrefslogtreecommitdiff
path: root/src/Jellyfin.MediaEncoding.Keyframes
diff options
context:
space:
mode:
authorcvium <clausvium@gmail.com>2022-01-11 23:30:30 +0100
committercvium <clausvium@gmail.com>2022-01-11 23:30:30 +0100
commit6ffa9539bbfbfb1090b02cebc8a28283a8c69041 (patch)
tree13f4a1d968780f90cd7d0c99e422970117a380f0 /src/Jellyfin.MediaEncoding.Keyframes
parentc658a883a2bc84b46ed73d209d2983e8a324cdce (diff)
Refactor and add scheduled task
Diffstat (limited to 'src/Jellyfin.MediaEncoding.Keyframes')
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs127
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/FfTool/FfToolKeyframeExtractor.cs23
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/KeyframeData.cs43
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/KeyframeExtractor.cs69
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Matroska/Extensions/EbmlReaderExtensions.cs257
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Matroska/MatroskaConstants.cs47
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Matroska/MatroskaKeyframeExtractor.cs101
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Matroska/Models/Info.cs43
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Matroska/Models/SeekHead.cs55
9 files changed, 344 insertions, 421 deletions
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs
index 351d880fe..320604e10 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs
+++ b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs
@@ -4,92 +4,91 @@ 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>
+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>
- /// FfProbe based keyframe extractor.
+ /// Extracts the keyframes using the ffprobe executable at the specified path.
/// </summary>
- public static class FfProbeKeyframeExtractor
+ /// <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)
{
- 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)
+ using var process = new Process
{
- using var process = new Process
+ StartInfo = new ProcessStartInfo
{
- StartInfo = new ProcessStartInfo
- {
- FileName = ffProbePath,
- Arguments = string.Format(CultureInfo.InvariantCulture, DefaultArguments, filePath),
+ FileName = ffProbePath,
+ Arguments = string.Format(CultureInfo.InvariantCulture, DefaultArguments, filePath),
- CreateNoWindow = true,
- UseShellExecute = false,
- RedirectStandardOutput = true,
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
- WindowStyle = ProcessWindowStyle.Hidden,
- ErrorDialog = false,
- },
- EnableRaisingEvents = true
- };
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false,
+ },
+ EnableRaisingEvents = true
+ };
- process.Start();
+ process.Start();
- return ParseStream(process.StandardOutput);
- }
+ return ParseStream(process.StandardOutput);
+ }
- internal static KeyframeData ParseStream(StreamReader reader)
- {
- var keyframes = new List<long>();
- double streamDuration = 0;
- double formatDuration = 0;
+ internal static KeyframeData ParseStream(StreamReader reader)
+ {
+ var keyframes = new List<long>();
+ double streamDuration = 0;
+ double formatDuration = 0;
- while (!reader.EndOfStream)
+ while (!reader.EndOfStream)
+ {
+ var line = reader.ReadLine().AsSpan();
+ if (line.IsEmpty)
{
- var line = reader.ReadLine().AsSpan();
- if (line.IsEmpty)
- {
- continue;
- }
+ continue;
+ }
- var firstComma = line.IndexOf(',');
- var lineType = line[..firstComma];
- var rest = line[(firstComma + 1)..];
- if (lineType.Equals("packet", StringComparison.OrdinalIgnoreCase))
+ var firstComma = line.IndexOf(',');
+ var lineType = line[..firstComma];
+ var rest = line[(firstComma + 1)..];
+ if (lineType.Equals("packet", StringComparison.OrdinalIgnoreCase))
+ {
+ if (rest.EndsWith(",K_"))
{
- 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));
- }
+ // 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))
+ }
+ else if (lineType.Equals("stream", StringComparison.OrdinalIgnoreCase))
+ {
+ if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var streamDurationResult))
{
- if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var streamDurationResult))
- {
- streamDuration = streamDurationResult;
- }
+ streamDuration = streamDurationResult;
}
- else if (lineType.Equals("format", StringComparison.OrdinalIgnoreCase))
+ }
+ else if (lineType.Equals("format", StringComparison.OrdinalIgnoreCase))
+ {
+ if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var formatDurationResult))
{
- if (double.TryParse(rest, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var formatDurationResult))
- {
- formatDuration = formatDurationResult;
- }
+ formatDuration = formatDurationResult;
}
}
+ }
- // Prefer the stream duration as it should be more accurate
- var duration = streamDuration > 0 ? streamDuration : formatDuration;
+ // Prefer the stream duration as it should be more accurate
+ var duration = streamDuration > 0 ? streamDuration : formatDuration;
- return new KeyframeData(TimeSpan.FromSeconds(duration).Ticks, keyframes);
- }
+ return new KeyframeData(TimeSpan.FromSeconds(duration).Ticks, keyframes);
}
}
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/FfTool/FfToolKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Keyframes/FfTool/FfToolKeyframeExtractor.cs
index fdd5dc577..aaaca6fe1 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/FfTool/FfToolKeyframeExtractor.cs
+++ b/src/Jellyfin.MediaEncoding.Keyframes/FfTool/FfToolKeyframeExtractor.cs
@@ -1,18 +1,17 @@
using System;
-namespace Jellyfin.MediaEncoding.Keyframes.FfTool
+namespace Jellyfin.MediaEncoding.Keyframes.FfTool;
+
+/// <summary>
+/// FfTool based keyframe extractor.
+/// </summary>
+public static class FfToolKeyframeExtractor
{
/// <summary>
- /// FfTool based keyframe extractor.
+ /// Extracts the keyframes using the fftool executable at the specified path.
/// </summary>
- public static class FfToolKeyframeExtractor
- {
- /// <summary>
- /// Extracts the keyframes using the fftool executable at the specified path.
- /// </summary>
- /// <param name="ffToolPath">The path to the fftool executable.</param>
- /// <param name="filePath">The file path.</param>
- /// <returns>An instance of <see cref="KeyframeData"/>.</returns>
- public static KeyframeData GetKeyframeData(string ffToolPath, string filePath) => throw new NotImplementedException();
- }
+ /// <param name="ffToolPath">The path to the fftool executable.</param>
+ /// <param name="filePath">The file path.</param>
+ /// <returns>An instance of <see cref="KeyframeData"/>.</returns>
+ public static KeyframeData GetKeyframeData(string ffToolPath, string filePath) => throw new NotImplementedException();
}
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/KeyframeData.cs b/src/Jellyfin.MediaEncoding.Keyframes/KeyframeData.cs
index 1683cd22a..06f9180e7 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/KeyframeData.cs
+++ b/src/Jellyfin.MediaEncoding.Keyframes/KeyframeData.cs
@@ -1,31 +1,30 @@
using System.Collections.Generic;
-namespace Jellyfin.MediaEncoding.Keyframes
+namespace Jellyfin.MediaEncoding.Keyframes;
+
+/// <summary>
+/// Keyframe information for a specific file.
+/// </summary>
+public class KeyframeData
{
/// <summary>
- /// Keyframe information for a specific file.
+ /// Initializes a new instance of the <see cref="KeyframeData"/> class.
/// </summary>
- public class KeyframeData
+ /// <param name="totalDuration">The total duration of the video stream in ticks.</param>
+ /// <param name="keyframeTicks">The video keyframes in ticks.</param>
+ public KeyframeData(long totalDuration, IReadOnlyList<long> keyframeTicks)
{
- /// <summary>
- /// Initializes a new instance of the <see cref="KeyframeData"/> class.
- /// </summary>
- /// <param name="totalDuration">The total duration of the video stream in ticks.</param>
- /// <param name="keyframeTicks">The video keyframes in ticks.</param>
- public KeyframeData(long totalDuration, IReadOnlyList<long> keyframeTicks)
- {
- TotalDuration = totalDuration;
- KeyframeTicks = keyframeTicks;
- }
+ TotalDuration = totalDuration;
+ KeyframeTicks = keyframeTicks;
+ }
- /// <summary>
- /// Gets the total duration of the stream in ticks.
- /// </summary>
- public long TotalDuration { get; }
+ /// <summary>
+ /// Gets the total duration of the stream in ticks.
+ /// </summary>
+ public long TotalDuration { get; }
- /// <summary>
- /// Gets the keyframes in ticks.
- /// </summary>
- public IReadOnlyList<long> KeyframeTicks { get; }
- }
+ /// <summary>
+ /// Gets the keyframes in ticks.
+ /// </summary>
+ public IReadOnlyList<long> KeyframeTicks { get; }
}
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/KeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Keyframes/KeyframeExtractor.cs
deleted file mode 100644
index 5304a55f8..000000000
--- a/src/Jellyfin.MediaEncoding.Keyframes/KeyframeExtractor.cs
+++ /dev/null
@@ -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>());
- }
- }
-}
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Matroska/Extensions/EbmlReaderExtensions.cs b/src/Jellyfin.MediaEncoding.Keyframes/Matroska/Extensions/EbmlReaderExtensions.cs
index 75d5aafe0..e068cac84 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Matroska/Extensions/EbmlReaderExtensions.cs
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Matroska/Extensions/EbmlReaderExtensions.cs
@@ -3,176 +3,175 @@ 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>
+internal static class EbmlReaderExtensions
{
/// <summary>
- /// Extension methods for the <see cref="EbmlReader"/> class.
+ /// Traverses the current container to find the element with <paramref name="identifier"/> identifier.
/// </summary>
- internal static class EbmlReaderExtensions
+ /// <param name="reader">An instance of <see cref="EbmlReader"/>.</param>
+ /// <param name="identifier">The element identifier.</param>
+ /// <returns>A value indicating whether the element was found.</returns>
+ internal static bool FindElement(this EbmlReader reader, ulong identifier)
{
- /// <summary>
- /// Traverses the current container to find the element with <paramref name="identifier"/> identifier.
- /// </summary>
- /// <param name="reader">An instance of <see cref="EbmlReader"/>.</param>
- /// <param name="identifier">The element identifier.</param>
- /// <returns>A value indicating whether the element was found.</returns>
- internal static bool FindElement(this EbmlReader reader, ulong identifier)
+ while (reader.ReadNext())
{
- while (reader.ReadNext())
+ if (reader.ElementId.EncodedValue == identifier)
{
- if (reader.ElementId.EncodedValue == identifier)
- {
- return true;
- }
+ return true;
}
-
- return false;
}
- /// <summary>
- /// Reads the current position in the file as an unsigned integer converted from binary.
- /// </summary>
- /// <param name="reader">An instance of <see cref="EbmlReader"/>.</param>
- /// <returns>The unsigned integer.</returns>
- internal static uint ReadUIntFromBinary(this EbmlReader reader)
+ return false;
+ }
+
+ /// <summary>
+ /// Reads the current position in the file as an unsigned integer converted from binary.
+ /// </summary>
+ /// <param name="reader">An instance of <see cref="EbmlReader"/>.</param>
+ /// <returns>The unsigned integer.</returns>
+ internal static uint ReadUIntFromBinary(this EbmlReader reader)
+ {
+ var buffer = new byte[4];
+ reader.ReadBinary(buffer, 0, 4);
+ return BinaryPrimitives.ReadUInt32BigEndian(buffer);
+ }
+
+ /// <summary>
+ /// Reads from the start of the file to retrieve the SeekHead segment.
+ /// </summary>
+ /// <param name="reader">An instance of <see cref="EbmlReader"/>.</param>
+ /// <returns>Instance of <see cref="SeekHead"/>.</returns>
+ internal static SeekHead ReadSeekHead(this EbmlReader reader)
+ {
+ reader = reader ?? throw new ArgumentNullException(nameof(reader));
+
+ if (reader.ElementPosition != 0)
{
- var buffer = new byte[4];
- reader.ReadBinary(buffer, 0, 4);
- return BinaryPrimitives.ReadUInt32BigEndian(buffer);
+ throw new InvalidOperationException("File position must be at 0");
}
- /// <summary>
- /// Reads from the start of the file to retrieve the SeekHead segment.
- /// </summary>
- /// <param name="reader">An instance of <see cref="EbmlReader"/>.</param>
- /// <returns>Instance of <see cref="SeekHead"/>.</returns>
- internal static SeekHead ReadSeekHead(this EbmlReader reader)
+ // Skip the header
+ if (!reader.FindElement(MatroskaConstants.SegmentContainer))
{
- reader = reader ?? throw new ArgumentNullException(nameof(reader));
-
- if (reader.ElementPosition != 0)
- {
- throw new InvalidOperationException("File position must be at 0");
- }
-
- // Skip the header
- if (!reader.FindElement(MatroskaConstants.SegmentContainer))
- {
- throw new InvalidOperationException("Expected a segment container");
- }
+ throw new InvalidOperationException("Expected a segment container");
+ }
- reader.EnterContainer();
+ reader.EnterContainer();
- long? tracksPosition = null;
- long? cuesPosition = null;
- long? infoPosition = null;
- // The first element should be a SeekHead otherwise we'll have to search manually
- if (!reader.FindElement(MatroskaConstants.SeekHead))
- {
- throw new InvalidOperationException("Expected a SeekHead");
- }
+ long? tracksPosition = null;
+ long? cuesPosition = null;
+ long? infoPosition = null;
+ // The first element should be a SeekHead otherwise we'll have to search manually
+ if (!reader.FindElement(MatroskaConstants.SeekHead))
+ {
+ throw new InvalidOperationException("Expected a SeekHead");
+ }
+ reader.EnterContainer();
+ while (reader.FindElement(MatroskaConstants.Seek))
+ {
reader.EnterContainer();
- while (reader.FindElement(MatroskaConstants.Seek))
+ reader.ReadNext();
+ var type = (ulong)reader.ReadUIntFromBinary();
+ switch (type)
{
- reader.EnterContainer();
- reader.ReadNext();
- var type = (ulong)reader.ReadUIntFromBinary();
- switch (type)
- {
- case MatroskaConstants.Tracks:
- reader.ReadNext();
- tracksPosition = (long)reader.ReadUInt();
- break;
- case MatroskaConstants.Cues:
- reader.ReadNext();
- cuesPosition = (long)reader.ReadUInt();
- break;
- case MatroskaConstants.Info:
- reader.ReadNext();
- infoPosition = (long)reader.ReadUInt();
- break;
- }
-
- reader.LeaveContainer();
-
- if (tracksPosition.HasValue && cuesPosition.HasValue && infoPosition.HasValue)
- {
+ case MatroskaConstants.Tracks:
+ reader.ReadNext();
+ tracksPosition = (long)reader.ReadUInt();
+ break;
+ case MatroskaConstants.Cues:
+ reader.ReadNext();
+ cuesPosition = (long)reader.ReadUInt();
+ break;
+ case MatroskaConstants.Info:
+ reader.ReadNext();
+ infoPosition = (long)reader.ReadUInt();
break;
- }
}
reader.LeaveContainer();
- if (!tracksPosition.HasValue || !cuesPosition.HasValue || !infoPosition.HasValue)
+ if (tracksPosition.HasValue && cuesPosition.HasValue && infoPosition.HasValue)
{
- throw new InvalidOperationException("SeekHead is missing or does not contain Info, Tracks and Cues positions");
+ break;
}
-
- return new SeekHead(infoPosition.Value, tracksPosition.Value, cuesPosition.Value);
}
- /// <summary>
- /// Reads from SegmentContainer to retrieve the Info segment.
- /// </summary>
- /// <param name="reader">An instance of <see cref="EbmlReader"/>.</param>
- /// <param name="position">The position of the info segment relative to the Segment container.</param>
- /// <returns>Instance of <see cref="Info"/>.</returns>
- internal static Info ReadInfo(this EbmlReader reader, long position)
+ reader.LeaveContainer();
+
+ if (!tracksPosition.HasValue || !cuesPosition.HasValue || !infoPosition.HasValue)
{
- reader.ReadAt(position);
+ throw new InvalidOperationException("SeekHead is missing or does not contain Info, Tracks and Cues positions");
+ }
- double? duration = null;
- reader.EnterContainer();
- // Mandatory element
- reader.FindElement(MatroskaConstants.TimestampScale);
- var timestampScale = reader.ReadUInt();
+ return new SeekHead(infoPosition.Value, tracksPosition.Value, cuesPosition.Value);
+ }
- if (reader.FindElement(MatroskaConstants.Duration))
- {
- duration = reader.ReadFloat();
- }
+ /// <summary>
+ /// Reads from SegmentContainer to retrieve the Info segment.
+ /// </summary>
+ /// <param name="reader">An instance of <see cref="EbmlReader"/>.</param>
+ /// <param name="position">The position of the info segment relative to the Segment container.</param>
+ /// <returns>Instance of <see cref="Info"/>.</returns>
+ internal static Info ReadInfo(this EbmlReader reader, long position)
+ {
+ reader.ReadAt(position);
- reader.LeaveContainer();
+ double? duration = null;
+ reader.EnterContainer();
+ // Mandatory element
+ reader.FindElement(MatroskaConstants.TimestampScale);
+ var timestampScale = reader.ReadUInt();
- return new Info((long)timestampScale, duration);
+ if (reader.FindElement(MatroskaConstants.Duration))
+ {
+ duration = reader.ReadFloat();
}
- /// <summary>
- /// Enters the Tracks segment and reads all tracks to find the specified type.
- /// </summary>
- /// <param name="reader">Instance of <see cref="EbmlReader"/>.</param>
- /// <param name="tracksPosition">The relative position of the tracks segment.</param>
- /// <param name="type">The track type identifier.</param>
- /// <returns>The first track number with the specified type.</returns>
- /// <exception cref="InvalidOperationException">Stream type is not found.</exception>
- internal static ulong FindFirstTrackNumberByType(this EbmlReader reader, long tracksPosition, ulong type)
- {
- reader.ReadAt(tracksPosition);
+ reader.LeaveContainer();
+ return new Info((long)timestampScale, duration);
+ }
+
+ /// <summary>
+ /// Enters the Tracks segment and reads all tracks to find the specified type.
+ /// </summary>
+ /// <param name="reader">Instance of <see cref="EbmlReader"/>.</param>
+ /// <param name="tracksPosition">The relative position of the tracks segment.</param>
+ /// <param name="type">The track type identifier.</param>
+ /// <returns>The first track number with the specified type.</returns>
+ /// <exception cref="InvalidOperationException">Stream type is not found.</exception>
+ internal static ulong FindFirstTrackNumberByType(this EbmlReader reader, long tracksPosition, ulong type)
+ {
+ reader.ReadAt(tracksPosition);
+
+ reader.EnterContainer();
+ while (reader.FindElement(MatroskaConstants.TrackEntry))
+ {
reader.EnterContainer();
- while (reader.FindElement(MatroskaConstants.TrackEntry))
- {
- reader.EnterContainer();
- // Mandatory element
- reader.FindElement(MatroskaConstants.TrackNumber);
- var trackNumber = reader.ReadUInt();
+ // Mandatory element
+ reader.FindElement(MatroskaConstants.TrackNumber);
+ var trackNumber = reader.ReadUInt();
- // Mandatory element
- reader.FindElement(MatroskaConstants.TrackType);
- var trackType = reader.ReadUInt();
+ // Mandatory element
+ reader.FindElement(MatroskaConstants.TrackType);
+ var trackType = reader.ReadUInt();
+ reader.LeaveContainer();
+ if (trackType == MatroskaConstants.TrackTypeVideo)
+ {
reader.LeaveContainer();
- if (trackType == MatroskaConstants.TrackTypeVideo)
- {
- reader.LeaveContainer();
- return trackNumber;
- }
+ return trackNumber;
}
+ }
- reader.LeaveContainer();
+ reader.LeaveContainer();
- throw new InvalidOperationException($"No stream with type {type} found");
- }
+ throw new InvalidOperationException($"No stream with type {type} found");
}
}
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Matroska/MatroskaConstants.cs b/src/Jellyfin.MediaEncoding.Keyframes/Matroska/MatroskaConstants.cs
index d18418d45..0d5c2f34f 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Matroska/MatroskaConstants.cs
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Matroska/MatroskaConstants.cs
@@ -1,31 +1,30 @@
-namespace Jellyfin.MediaEncoding.Keyframes.Matroska
+namespace Jellyfin.MediaEncoding.Keyframes.Matroska;
+
+/// <summary>
+/// Constants for the Matroska identifiers.
+/// </summary>
+public static class MatroskaConstants
{
- /// <summary>
- /// Constants for the Matroska identifiers.
- /// </summary>
- public static class MatroskaConstants
- {
- internal const ulong SegmentContainer = 0x18538067;
+ internal const ulong SegmentContainer = 0x18538067;
- internal const ulong SeekHead = 0x114D9B74;
- internal const ulong Seek = 0x4DBB;
+ internal const ulong SeekHead = 0x114D9B74;
+ internal const ulong Seek = 0x4DBB;
- internal const ulong Info = 0x1549A966;
- internal const ulong TimestampScale = 0x2AD7B1;
- internal const ulong Duration = 0x4489;
+ internal const ulong Info = 0x1549A966;
+ internal const ulong TimestampScale = 0x2AD7B1;
+ internal const ulong Duration = 0x4489;
- internal const ulong Tracks = 0x1654AE6B;
- internal const ulong TrackEntry = 0xAE;
- internal const ulong TrackNumber = 0xD7;
- internal const ulong TrackType = 0x83;
+ internal const ulong Tracks = 0x1654AE6B;
+ internal const ulong TrackEntry = 0xAE;
+ internal const ulong TrackNumber = 0xD7;
+ internal const ulong TrackType = 0x83;
- internal const ulong TrackTypeVideo = 0x1;
- internal const ulong TrackTypeSubtitle = 0x11;
+ internal const ulong TrackTypeVideo = 0x1;
+ internal const ulong TrackTypeSubtitle = 0x11;
- internal const ulong Cues = 0x1C53BB6B;
- internal const ulong CueTime = 0xB3;
- internal const ulong CuePoint = 0xBB;
- internal const ulong CueTrackPositions = 0xB7;
- internal const ulong CuePointTrackNumber = 0xF7;
- }
+ internal const ulong Cues = 0x1C53BB6B;
+ internal const ulong CueTime = 0xB3;
+ internal const ulong CuePoint = 0xBB;
+ internal const ulong CueTrackPositions = 0xB7;
+ internal const ulong CuePointTrackNumber = 0xF7;
}
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Matroska/MatroskaKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Keyframes/Matroska/MatroskaKeyframeExtractor.cs
index 6a8a55643..8bb1ff00d 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Matroska/MatroskaKeyframeExtractor.cs
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Matroska/MatroskaKeyframeExtractor.cs
@@ -4,73 +4,72 @@ 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>
+public static class MatroskaKeyframeExtractor
{
/// <summary>
- /// The keyframe extractor for the matroska container.
+ /// Extracts the keyframes in ticks (scaled using the container timestamp scale) from the matroska container.
/// </summary>
- public static class MatroskaKeyframeExtractor
+ /// <param name="filePath">The file path.</param>
+ /// <returns>An instance of <see cref="KeyframeData"/>.</returns>
+ public static KeyframeData GetKeyframeData(string filePath)
{
- /// <summary>
- /// Extracts the keyframes in ticks (scaled using the container timestamp scale) from the matroska container.
- /// </summary>
- /// <param name="filePath">The file path.</param>
- /// <returns>An instance of <see cref="KeyframeData"/>.</returns>
- public static KeyframeData GetKeyframeData(string filePath)
- {
- using var stream = File.OpenRead(filePath);
- using var reader = new EbmlReader(stream);
+ using var stream = File.OpenRead(filePath);
+ using var reader = new EbmlReader(stream);
+
+ var seekHead = reader.ReadSeekHead();
+ var info = reader.ReadInfo(seekHead.InfoPosition);
+ var videoTrackNumber = reader.FindFirstTrackNumberByType(seekHead.TracksPosition, MatroskaConstants.TrackTypeVideo);
- var seekHead = reader.ReadSeekHead();
- var info = reader.ReadInfo(seekHead.InfoPosition);
- var videoTrackNumber = reader.FindFirstTrackNumberByType(seekHead.TracksPosition, MatroskaConstants.TrackTypeVideo);
+ var keyframes = new List<long>();
+ reader.ReadAt(seekHead.CuesPosition);
+ reader.EnterContainer();
- var keyframes = new List<long>();
- reader.ReadAt(seekHead.CuesPosition);
+ while (reader.FindElement(MatroskaConstants.CuePoint))
+ {
reader.EnterContainer();
+ ulong? trackNumber = null;
+ // Mandatory element
+ reader.FindElement(MatroskaConstants.CueTime);
+ var cueTime = reader.ReadUInt();
- while (reader.FindElement(MatroskaConstants.CuePoint))
+ // Mandatory element
+ reader.FindElement(MatroskaConstants.CueTrackPositions);
+ reader.EnterContainer();
+ if (reader.FindElement(MatroskaConstants.CuePointTrackNumber))
{
- reader.EnterContainer();
- ulong? trackNumber = null;
- // Mandatory element
- reader.FindElement(MatroskaConstants.CueTime);
- var cueTime = reader.ReadUInt();
-
- // Mandatory element
- reader.FindElement(MatroskaConstants.CueTrackPositions);
- reader.EnterContainer();
- if (reader.FindElement(MatroskaConstants.CuePointTrackNumber))
- {
- trackNumber = reader.ReadUInt();
- }
-
- reader.LeaveContainer();
+ trackNumber = reader.ReadUInt();
+ }
- if (trackNumber == videoTrackNumber)
- {
- keyframes.Add(ScaleToTicks(cueTime, info.TimestampScale));
- }
+ reader.LeaveContainer();
- reader.LeaveContainer();
+ if (trackNumber == videoTrackNumber)
+ {
+ keyframes.Add(ScaleToTicks(cueTime, info.TimestampScale));
}
reader.LeaveContainer();
-
- var result = new KeyframeData(ScaleToTicks(info.Duration ?? 0, info.TimestampScale), keyframes);
- return result;
}
- 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;
- }
+ reader.LeaveContainer();
- 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);
- }
+ var result = new KeyframeData(ScaleToTicks(info.Duration ?? 0, info.TimestampScale), keyframes);
+ return result;
+ }
+
+ 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 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);
}
}
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Matroska/Models/Info.cs b/src/Jellyfin.MediaEncoding.Keyframes/Matroska/Models/Info.cs
index 02c6741ec..415d6da00 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Matroska/Models/Info.cs
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Matroska/Models/Info.cs
@@ -1,29 +1,28 @@
-namespace Jellyfin.MediaEncoding.Keyframes.Matroska.Models
+namespace Jellyfin.MediaEncoding.Keyframes.Matroska.Models;
+
+/// <summary>
+/// The matroska Info segment.
+/// </summary>
+internal class Info
{
/// <summary>
- /// The matroska Info segment.
+ /// Initializes a new instance of the <see cref="Info"/> class.
/// </summary>
- internal class Info
+ /// <param name="timestampScale">The timestamp scale in nanoseconds.</param>
+ /// <param name="duration">The duration of the entire file.</param>
+ public Info(long timestampScale, double? duration)
{
- /// <summary>
- /// Initializes a new instance of the <see cref="Info"/> class.
- /// </summary>
- /// <param name="timestampScale">The timestamp scale in nanoseconds.</param>
- /// <param name="duration">The duration of the entire file.</param>
- public Info(long timestampScale, double? duration)
- {
- TimestampScale = timestampScale;
- Duration = duration;
- }
+ TimestampScale = timestampScale;
+ Duration = duration;
+ }
- /// <summary>
- /// Gets the timestamp scale in nanoseconds.
- /// </summary>
- public long TimestampScale { get; }
+ /// <summary>
+ /// Gets the timestamp scale in nanoseconds.
+ /// </summary>
+ public long TimestampScale { get; }
- /// <summary>
- /// Gets the total duration of the file.
- /// </summary>
- public double? Duration { get; }
- }
+ /// <summary>
+ /// Gets the total duration of the file.
+ /// </summary>
+ public double? Duration { get; }
}
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Matroska/Models/SeekHead.cs b/src/Jellyfin.MediaEncoding.Keyframes/Matroska/Models/SeekHead.cs
index d9e346c03..95e4fd882 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Matroska/Models/SeekHead.cs
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Matroska/Models/SeekHead.cs
@@ -1,36 +1,35 @@
-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>
+internal class SeekHead
{
/// <summary>
- /// The matroska SeekHead segment. All positions are relative to the Segment container.
+ /// Initializes a new instance of the <see cref="SeekHead"/> class.
/// </summary>
- internal class SeekHead
+ /// <param name="infoPosition">The relative file position of the info segment.</param>
+ /// <param name="tracksPosition">The relative file position of the tracks segment.</param>
+ /// <param name="cuesPosition">The relative file position of the cues segment.</param>
+ public SeekHead(long infoPosition, long tracksPosition, long cuesPosition)
{
- /// <summary>
- /// Initializes a new instance of the <see cref="SeekHead"/> class.
- /// </summary>
- /// <param name="infoPosition">The relative file position of the info segment.</param>
- /// <param name="tracksPosition">The relative file position of the tracks segment.</param>
- /// <param name="cuesPosition">The relative file position of the cues segment.</param>
- public SeekHead(long infoPosition, long tracksPosition, long cuesPosition)
- {
- InfoPosition = infoPosition;
- TracksPosition = tracksPosition;
- CuesPosition = cuesPosition;
- }
+ InfoPosition = infoPosition;
+ TracksPosition = tracksPosition;
+ CuesPosition = cuesPosition;
+ }
- /// <summary>
- /// Gets relative file position of the info segment.
- /// </summary>
- public long InfoPosition { get; }
+ /// <summary>
+ /// Gets relative file position of the info segment.
+ /// </summary>
+ public long InfoPosition { get; }
- /// <summary>
- /// Gets the relative file position of the tracks segment.
- /// </summary>
- public long TracksPosition { get; }
+ /// <summary>
+ /// Gets the relative file position of the tracks segment.
+ /// </summary>
+ public long TracksPosition { get; }
- /// <summary>
- /// Gets the relative file position of the cues segment.
- /// </summary>
- public long CuesPosition { get; }
- }
+ /// <summary>
+ /// Gets the relative file position of the cues segment.
+ /// </summary>
+ public long CuesPosition { get; }
}