Cap video stills inside ffmpeg instead of after the pipe - #413
Merged
Merged
Conversation
Frame extraction asked ffmpeg for a native-resolution PNG and shrank it in Python, so a 4K frame crossed the pipe at ~11 MB (more for 10-bit HEVC). The filtergraph now squares pixels first, then fits the frame inside a MAX_FRAME_EDGE box, which keeps anamorphic sources at their display aspect and caps portrait video on its height. The source resolution can no longer be read off the capped frame, so a showinfo filter between the two scales logs it. Builds without showinfo leave it out (probed once per binary) and fall back to the frame's own size when uncapped. The input path is removed from ffmpeg's stderr before parsing so a filename containing newlines cannot forge report lines. Closes #366 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lstein
enabled auto-merge (squash)
September 29, 2026 22:32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #366.
Problem
_frame_commandasked ffmpeg for a PNG at the source's native resolution, and theMAX_FRAME_EDGEcap was applied afterwards in Python. A 4K frame therefore crossed the pipe and was decoded at full size, about 11 MB each (more for 10-bit HEVC), times the number of concurrent loaders.Fix
min(…, iw)means small sources are never upscaled. The 2048 is taken fromMAX_FRAME_EDGE.showinfo.VideoInfo.width/heightwere read from the decoded frame, so ffmpeg-side capping would have labelled every 4K video 2048-wide.showinfologs the geometry after rotation and SAR correction but before the cap._showinfo_size()parses it from the metadata-stripped report.frame.thumbnail(...)stays as a backstop.Hardening from adversarial review
showinfo: a missing filter fails the whole graph, which would drop every video._ffmpeg_has_showinfo()probesffmpeg -filtersonce per binary and leaves the filter out if it's absent. A failed probe is not cached. Without it, the resolution falls back to the frame's own size when uncapped, or is left blank at the cap.Other attacks the review tried that behaved correctly: 90°, 180° and flip rotation; SAR 2, 1/100 and 1/64; needle-shaped 2x16384 sources; rgba64 and 10-bit HEVC; a mid-stream resolution change with accurate seek (the first
showinfoline always matched the frame that was output, on both ffmpeg 7.0.2 and 6.1);\rprogress lines; and consumers ofwidth/heighthandlingNone.Measurements
End-to-end through
extract_video_frame, bundled ffmpeg 7.0.2, noisy synthetic clips:The stored stills and the reported resolutions are the same as before.
Notes
FRAME_SELECTION_GENERATIONis not bumped. ffmpeg's scaler differs slightly from PIL's, which could in rare cases change which frame is picked. That doesn't seem worth making every album re-extract its stills.-vf null. It predates this change and is left alone here.Tests
New tests in
test_video_probe.pycover:showinfoparsing, and tag-injection attempts;showinfofilter;The full backend suite passes (1207), and ruff is clean.
🤖 Generated with Claude Code