Two bugs the library run exposed on real files:
* An input seek is not a faithful decode. On an H.264 source with open GOPs the frames after the
landing point decode differently from a sequential decode - the SAME file compared with itself,
once seeked and once decoded in order, scored min 0.25 / mean 0.82, and the gate condemned good
encodes because of it. Both sides are now decoded sequentially with the window cut by the trim
filter, which is also the only decode guaranteed to match what the encoder itself saw.
* Staleness of the run lock. Killing a background session kills the wrapper but not the python
underneath, so a live run's lock survives and the next start is correctly refused (it looked like
a pid-reuse bug; it was not). The lock now carries a heartbeat, staleness is decided by age as
well as pid, and a takeover that cannot delete a file the OS still holds overwrites it in place.
Give it a path; it reads the source, picks the recipe, encodes, and verifies the
result before accepting it.
The problem it exists to remove: a file can drop ~23% of its frames in a browser
while r_frame_rate == avg_frame_rate reads EXACTLY equal, so every fps-based check
calls it healthy and it never enters a repair queue. The cause is deterministic and
header-readable -- the MP4 composition-offset table is unusable, either a single
constant ctts entry (PTS == DTS + const) or no ctts box at all (PTS == DTS) -- so the
frame timestamps are in decode order and the decoder's reordered output frames carry
the wrong times.
- three recipes chosen from the source FILE, never the filename: lossless -c copy when
the timeline allows it, AAC-only fix when just the audio is wrong, full HEVC (or
x264 for reach) when the timeline cannot be copied
- quality policy: never upscale; HEVC -cq 27 measured at SSIM 0.9937 ~= source size;
audio copied when already AAC, else AAC 256k
- the two flags that must never be dropped: -fps_mode cfr and -pix_fmt yuv420p
- verified before acceptance: duration, resolution, codec, hvc1 tag, 8-bit, uniform
cadence, no flat composition offsets, full decode pass, and a read-back after the write
- built for cron: lock file (two concurrent NVENC jobs starve one encoder and DROP
FRAMES), idempotent, leaves the input untouched on failure, aborts below 5 GB free
- scripts/selftest.py builds both defect shapes and asserts the detector, including a
Matroska case that must NOT be flagged
Measured on the file that prompted this: 23.0% of frames dropped before, 0.0% after.