Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions PlexCleaner/FfMpegTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,8 @@ public VerifyResult VerifyMedia(string fileName)
}
if (verifyResult == VerifyResult.DecodeError)
{
// A silent non-zero exit has no error line, omit the empty field rather than logging blank
string error = CleanForLog(classifier.FirstError ?? string.Empty);
// Log the unique error lines, a silent non-zero exit has none so omit the empty field
string error = CleanForLog(string.Join(" | ", classifier.Errors));
if (string.IsNullOrEmpty(error))
{
Log.Error(
Expand Down
41 changes: 34 additions & 7 deletions PlexCleaner/VerifyClassifier.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
using System.Text.RegularExpressions;

namespace PlexCleaner;

internal static class VerifyClassifier
internal static partial class VerifyClassifier
{
// Non-monotonic-DTS muxer warnings emitted by the -f null muxer at error loglevel
private static readonly List<string> s_timestampSignatures =
[
"non monotonically increasing dts to muxer",
];

// Mask pointer addresses and standalone numbers so lines differing only by those collapse to one
// key; word boundaries keep digits inside identifiers like h264 or mpeg2 so distinct codecs stay apart
[GeneratedRegex(@"0x[0-9a-fA-F]+|\b[0-9]+\b")]
private static partial Regex VariableDataRegex();

private static string NormalizeKey(string line) => VariableDataRegex().Replace(line, "*");

public static VerifyResult Classify(string stderr)
{
Accumulator accumulator = new();
Expand All @@ -23,13 +32,21 @@ public static VerifyResult Classify(string stderr)

public sealed class Accumulator
{
private bool _decodeError;
// Backstop against a pathological file with many distinct error types
private const int MaxErrorLines = 50;

private bool _timestamp;

public string? FirstError { get; private set; }
// Unique decode-error lines kept for the failure log, deduped by normalized key, insertion ordered
private readonly HashSet<string> _errorKeys = [];
private readonly List<string> _errors = [];

public bool HasErrors => _errors.Count > 0;

public IReadOnlyList<string> Errors => _errors;

public VerifyResult Result =>
_decodeError ? VerifyResult.DecodeError
HasErrors ? VerifyResult.DecodeError
: _timestamp ? VerifyResult.TimestampOnly
: VerifyResult.Clean;

Expand All @@ -41,6 +58,13 @@ public void Add(string line)
return;
}

// ffmpeg collapses consecutive identical messages into this marker, it repeats the previous
// line's classification which is already recorded, so ignore it
if (line.StartsWith("Last message repeated", StringComparison.Ordinal))
{
return;
}

// A muxer timestamp warning classifies as TimestampOnly, distinct from a decode error
if (
s_timestampSignatures.Any(sig =>
Expand All @@ -52,9 +76,12 @@ public void Add(string line)
return;
}

// Anything else at error loglevel is treated as decode corruption, fail closed
_decodeError = true;
FirstError ??= line;
// Any other line at error loglevel is a decode error, fail closed; keep the unique lines for
// the log, deduped by normalized key and capped as a backstop
if (_errors.Count < MaxErrorLines && _errorKeys.Add(NormalizeKey(line)))
{
_errors.Add(line);
}
}
}
}
47 changes: 33 additions & 14 deletions PlexCleanerTests/VerifyClassifierTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,27 @@ public void Classify_OnlyDtsMuxerWarnings_ReturnsTimestampOnly()
_ = VerifyClassifier.Classify(stderr).Should().Be(VerifyResult.TimestampOnly);
}

[Fact]
public void Classify_DtsWarningFollowedByRepeatMarker_ReturnsTimestampOnly()
{
// ffmpeg collapses consecutive identical DTS warnings, the repeat marker must not fail the file
string stderr =
"[null @ 0x1] Application provided invalid, non monotonically increasing dts to muxer in stream 1: 8 >= 8\n"
+ " Last message repeated 1 times\n";
_ = VerifyClassifier.Classify(stderr).Should().Be(VerifyResult.TimestampOnly);
}

[Theory]
[InlineData("[matroska @ 0x1] Invalid data found when processing input")]
[InlineData("[h264 @ 0x1] error while decoding MB 10 20")]
[InlineData("[NULL @ 0x1] Invalid NAL unit size (-1148261185 > 8772).")]
[InlineData("[h264 @ 0x1] mmco: unref short failure")]
[InlineData("[aac @ 0x1] env_facs_q 255 is invalid")]
[InlineData("[matroska,webm @ 0x1] Length 7 indicated by an EBML number exceeds max length 4.")]
public void Classify_DecodeSignature_ReturnsDecodeError(string stderr) =>
[InlineData("[truehd @ 0x1] quant_step_size larger than huff_lsbs")]
public void Classify_DecodeError_ReturnsDecodeError(string stderr) =>
VerifyClassifier.Classify(stderr).Should().Be(VerifyResult.DecodeError);

[Fact]
public void Classify_DecodeErrorMixedWithDtsWarning_ReturnsDecodeError()
{
// A decode error co-occurring with benign DTS warnings must still fail
string stderr =
"[null @ 0x1] Application provided invalid, non monotonically increasing dts to muxer in stream 1: 8 >= 8\n"
+ "[h264 @ 0x1] error while decoding MB 3 4\n";
Expand All @@ -51,19 +58,31 @@ public void Classify_UnrecognizedStderr_ReturnsDecodeError() =>
.Be(VerifyResult.DecodeError);

[Fact]
public void Accumulator_StreamedLines_ClassifiesAndKeepsFirstError()
public void Accumulator_RepeatedErrorType_DedupedToUniqueLines()
{
// The accumulator sees lines one at a time without buffering the whole stderr
// Lines differing only by pointer address or numbers collapse to one entry, distinct types do not
VerifyClassifier.Accumulator accumulator = new();
accumulator.Add(
"[null @ 0x1] Application provided invalid, non monotonically increasing dts to muxer in stream 1: 8 >= 8"
);
_ = accumulator.Result.Should().Be(VerifyResult.TimestampOnly);
accumulator.Add("[pgssub @ 0x5f38] Unknown subtitle segment type 0x78, length 55981");
accumulator.Add("[pgssub @ 0x5f38] Unknown subtitle segment type 0x93, length 2183");
accumulator.Add("[aac @ 0x62] Prediction is not allowed in AAC-LC");

_ = accumulator.Result.Should().Be(VerifyResult.DecodeError);
_ = accumulator.Errors.Should().HaveCount(2);
_ = accumulator
.Errors[0]
.Should()
.Be("[pgssub @ 0x5f38] Unknown subtitle segment type 0x78, length 55981");
_ = accumulator.Errors[1].Should().Be("[aac @ 0x62] Prediction is not allowed in AAC-LC");
}

[Fact]
public void Accumulator_DifferentCodecSameMessage_NotDeduped()
{
// Digits inside a codec identifier must not be masked, so distinct codecs stay separate
VerifyClassifier.Accumulator accumulator = new();
accumulator.Add("[h264 @ 0x1] error while decoding MB 3 4");
accumulator.Add("[h264 @ 0x1] Invalid data found");
accumulator.Add("[h265 @ 0x2] error while decoding MB 3 4");

_ = accumulator.Result.Should().Be(VerifyResult.DecodeError);
_ = accumulator.FirstError.Should().Be("[h264 @ 0x1] error while decoding MB 3 4");
_ = accumulator.Errors.Should().HaveCount(2);
}
}