Add lossless DTS timestamp repair and verify reclassification - #833
Conversation
Verify no longer fails on benign non-monotonic DTS muxer warnings. ffmpeg -f null can exit 0 yet emit "non monotonically increasing dts to muxer" for files that decode and play; the old "any stderr fails" rule made these a permanent VerifyFailed/RepairFailed that a re-encode could not fix. Verify now classifies stderr line by line (streamed, so memory stays bounded), fail-closed: only a recognized benign timestamp warning passes, anything else fails. A timestamp-only result is a repairable failure, cleaned losslessly with the setts bitstream filter and gated by a byte-identical stream-hash check, ahead of the re-encode tier used for genuine decode corruption. Also consolidates the bitrate and DTS packet analyses into one show_packets pass, switches closed-caption detection to ffprobe analyze_frames, and adds the DtsTimestampRepair example plugin that retroactively clears RepairFailed files an older version gave up on. See #827.
There was a problem hiding this comment.
Pull request overview
This PR updates PlexCleaner's verify/repair pipeline to distinguish benign FFmpeg muxer timestamp warnings from real decode corruption, adds a lossless timestamp-repair tier, and modernizes closed-caption detection, with supporting tests, plugin example, and release docs/version bump.
Changes:
- Add
VerifyResult+VerifyClassifierand switch FFmpeg verify to streamed stderr classification (bounded memory). - Add lossless timestamp repair (
setts+streamhashregression gate) and consolidate packet analysis into a singleffprobe -show_packetspass. - Switch closed-caption detection to
ffprobe -analyze_framesand add theDtsTimestampRepairexample plugin + tests/docs updates.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| version.json | Bumps version floor to 3.21. |
| README.md | Adds 3.21 release notes + docs for non-monotonic DTS repair and the new plugin example. |
| HISTORY.md | Adds detailed 3.21 changelog entry for verify reclassification, timestamp repair, and CC probe changes. |
| cspell.json | Adds new technical terms used by the changes (e.g., setts, streamhash). |
| PlexCleaner/VerifyResult.cs | Introduces deterministic verify classification enum. |
| PlexCleaner/VerifyClassifier.cs | Implements fail-closed stderr line classification (timestamp-only vs decode error). |
| PlexCleaner/FfMpegTool.cs | Returns VerifyResult, streams stderr classification, adds SetTimestamps + GetStreamHashes. |
| PlexCleaner/MediaTool.cs | Adds stderr streaming execution helper used by verify classification. |
| PlexCleaner/ProcessFile.cs | Integrates verify classification, timestamp-only repair tier, packet analysis consolidation, and new CC probe path. |
| PlexCleaner/DtsInfo.cs | Adds per-stream DTS monotonicity tracking used to gate timestamp repair. |
| PlexCleaner/FfProbeTool.cs | Replaces lavfi subcc approach with -analyze_frames CC detection; consolidates packet probe entrypoint. |
| PlexCleaner/FfProbeBuilder.cs | Adds ReadIntervalFrames() helper for quick-scan bounding in CC probing. |
| PlexCleaner/FfMpegBuilder.cs | Adds -bsf:a builder support for setts. |
| PlexCleaner/FfMpegToolJsonSchema.cs | Adds JSON schema types for closed-captions probe output. |
| PlexCleaner/Program.cs | Introduces a quick-scan frame-count constant for CC probing. |
| PlexCleaner.slnx | Adds the new plugin project to the solution. |
| Plugins/DtsTimestampRepair/DtsTimestampRepairPlugin.cs | Adds example plugin to revisit legacy RepairFailed timestamp-only cases. |
| Plugins/DtsTimestampRepair/DtsTimestampRepair.csproj | Adds plugin project definition referencing PlexCleaner. |
| PlexCleanerTests/VerifyClassifierTests.cs | Adds unit tests for stderr classification + streamed accumulator behavior. |
| PlexCleanerTests/DtsInfoTests.cs | Adds unit tests for DTS monotonicity detection. |
| PlexCleanerTests/ClosedCaptionsProbeTests.cs | Adds unit tests for the new closed-captions probe JSON parsing. |
| PlexCleanerTests/PluginLoaderTests.cs | Extends plugin-loader tests to cover the new example plugin. |
| PlexCleanerTests/PlexCleanerTests.csproj | Adds the new plugin project reference so tests can load it. |
| PlexCleanerTests/FileNameEscapingTests.cs | Removes tests for the retired lavfi movie=...[out0+subcc] escaping path. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Address Copilot review: - The lossless timestamp repair rewrites the media, so log the decision once at Warning before the rewrite, per the AGENTS.md logging taxonomy, so it shows at --loglevel Warning. - A non-monotonic DTS is a correctable verify failure, not a pass; reword README and HISTORY so they no longer imply verify passes it.
Uh oh!
There was an error while loading. Please reload this page.
A malformed or empty ffprobe stdout made ClosedCaptionsProbe.FromJson throw and crash processing; catch and log like GetMediaPropsFromJson, returning false instead.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Log a non-monotonic DTS (TimestampOnly) verify outcome so the standalone verify command shows why it failed. - Omit the empty error field when a silent non-zero ffmpeg exit has no captured error line, matching LogFailedResult.
Uh oh!
There was an error while loading. Please reload this page.
TryLosslessTimestampRepair returned false for both a benign demux-clean file and a genuine repair failure, so a demux-visible DTS whose setts rewrite failed was wrongly marked Verified and never retried. Split the outcome into NotApplicable, Repaired, and Failed: Failed now sets VerifyFailed/RepairFailed, and cancellation leaves the state untouched.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Accepting a timestamp-only re-verify marked a file Verified even though it still fails verification, so a future run would skip it as already verified and hide the remaining failure.
Uh oh!
There was an error while loading. Please reload this page.
Previously a non-zero exit with only the benign DTS warning on stderr stayed TimestampOnly; fail closed and classify any non-zero exit as a decode error.
Uh oh!
There was an error while loading. Please reload this page.
A non-monotonic DTS that verify detects but the lossless setts repair cannot fix now stays RepairFailed instead of being cleared to Verified. Only a successful lossless repair or a clean re-verify clears the flag. Follow-up to #833.
ffmpeg's 'Last message repeated N times' line was misclassified as a decode error, so a duplicate-DTS file was wrongly failed instead of losslessly repaired. Ignore those markers. Also log the unique decode-error lines (deduped by a normalized key, capped) so a failure reports every distinct error. Follow-up to #833/#834.
Summary
Verify no longer fails on benign non-monotonic DTS muxer warnings (see #827).
ffmpeg -f nullcan exit0yet emitApplication provided invalid, non monotonically increasing dts to muxerfor files that decode and play correctly; the old "any stderr means failure" rule made these a permanentVerifyFailed/RepairFailed, and a re-encode could not fix it because Matroska stores no DTS and ffmpeg re-derives a non-monotonic timeline on read.Changes
VerifyClassifierpartitions the-f nullstderr line by line as it streams (O(1) memory - a non-monotonic-DTS file emits one warning per packet). Only a recognized benign timestamp warning passes; anything else, including an unrecognized line, fails. Verify is deterministic (Clean/TimestampOnly/DecodeError);AutoRepairgates only whether the failure is repaired.TimestampOnlyfailure is repaired with thesettsbitstream filter (stream copy, no re-encode), gated by a byte-identical per-streamstreamhashcheck, as the first repair tier ahead of the re-encode tier used for genuine decode corruption.ffprobe -show_packetspass.analyze_framesclosed captions: closed-caption detection usesffprobe -analyze_frames -show_entries stream=closed_captions, retiring themovie=...[out0+subcc]lavfi filter and its QuickScan snippet-remux workaround.DtsTimestampRepairplugin: revisits files an older version markedRepairFailed, clears the flag when the failure was a benign timestamp issue, and losslessly repairs the timestamps when the DTS is demux-visible.Testing
VerifyClassifier,DtsInfo,ClosedCaptionsJSON, plugin loader); CSharpier,dotnet format style, markdownlint, shellcheck, cspell all clean.processon a demux-visible DTS file (system ffmpeg 7.1.5) ->Repaired, Verified; a post-decode DTS file ->Verifiedwith no media change; the plugin on a seededRepairFailedcopy -> flag cleared,Verified+Repaired.analyze_framesCC path validated end-to-end against ffmpeg 8.0.1 ->ClearedCaptions, Verified.🤖 Generated with Claude Code