fix(cli): fix reencode deadlock at 0 frames; default .mp4 output + --replace#507
Merged
Conversation
`sio reencode <video>` (ffmpeg fast path) could hang forever at `0/X frames` with the rich spinner still animating and Ctrl+C unresponsive. Root cause: `_run_ffmpeg_with_progress` ran ffmpeg with `stderr=PIPE` but only read stdout (`-progress pipe:1`) during the encode, draining stderr only after the process exited. ffmpeg still writes its banner, stream info, and periodic stats to stderr; once that pipe buffer fills (~4 KB on Windows, ~64 KB on Linux) ffmpeg blocks on the stderr write, stops emitting stdout progress, and `stdout.readline()` parks forever. The spinner kept spinning because rich animates on its own thread, masking the dead main thread. Fix: - Split the run logic into `_run_subprocess_with_progress`, which drains stderr concurrently on a background thread (collected for error reporting) so the child never blocks. `_run_ffmpeg_with_progress` now just inserts the progress flags and delegates. - Tear down the ffmpeg child on `KeyboardInterrupt` (and via a `finally` safety net) so Ctrl+C aborts cleanly instead of orphaning ffmpeg. Add regression tests that feed a stand-in child emitting >pipe-buffer stderr with `frame=N` progress; these deadlock under the old code and pass now. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KpHzNYmZ7gzJmhck23ZS2M
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #507 +/- ##
==========================================
- Coverage 93.04% 92.98% -0.06%
==========================================
Files 54 54
Lines 19532 19588 +56
Branches 4412 4424 +12
==========================================
+ Hits 18173 18214 +41
- Misses 647 661 +14
- Partials 712 713 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Contributor
Docs PreviewPreview has been removed. |
…ace)
Two reencode UX enhancements:
- Default output is now always `{stem}.reencoded.mp4` regardless of the input
container. Reencoding always produces H.264/MP4, so keeping a `.mov`/`.mkv`/
`.avi` extension on the output was misleading; the `.reencoded` infix keeps it
distinct from the source even when the input is already `.mp4`.
- New `--replace` flag for a destructive in-place reencode: encodes to a temp
file in the destination directory, atomically moves it over `{stem}.mp4`, and
deletes the original if the extension changed (e.g. `foo.mov` -> `foo.mp4`,
removing `foo.mov`). ffmpeg can't read and write the same file in one pass, so
the temp+rename is required; the temp is cleaned up on failure/interrupt.
`--replace` is mutually exclusive with `-o/--output` and unsupported for SLP
inputs. Clobbering an unrelated existing `{stem}.mp4` still requires
`--overwrite`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KpHzNYmZ7gzJmhck23ZS2M
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KpHzNYmZ7gzJmhck23ZS2M
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.
Summary
Fixes a hard hang in
sio reencodeand adds two requested UX enhancements. Three logical changes (separate commits):sio reencode(ffmpeg path) could hang forever at0/X frames— spinner still animating, Ctrl+C unresponsive.{stem}.reencoded.mp4(no longer keeps.mov/.mkv/.avi).--replaceflag for destructive in-place reencode.1. The deadlock (root cause)
_run_ffmpeg_with_progressran ffmpeg withstderr=subprocess.PIPEbut, during the encode, only read stdout (-progress pipe:1); stderr was read only after the process exited. ffmpeg still writes its banner, stream info, and periodicframe= fps=stats to stderr even with-progress pipe:1. Once the stderr pipe buffer fills, ffmpeg blocks on the stderr write → it stops emitting stdout progress →stdout.readline()blocks forever. The spinner keeps spinning because rich animates on a separate thread, masking the dead main thread; Ctrl+C can't be delivered while the main thread is parked in a blocking pipe read.Measured (real
drone-clip.mov, HEVC 1080p60, 11.46s)Hit on essentially any real-length video; worse on Windows (4 KB pipe buffer vs Linux's 64 KB), which is why short CI fixtures never tripped it.
Fix
_run_subprocess_with_progress(testable core) from_run_ffmpeg_with_progress(which just inserts the progress flags and delegates).KeyboardInterrupt(+finallysafety net) so Ctrl+C aborts cleanly instead of orphaning ffmpeg.2. Default output is always
.mp4Reencoding always produces H.264/MP4, so keeping the source container extension on the output was misleading. The default is now
{stem}.reencoded.mp4regardless of input (the.reencodedinfix keeps it distinct even for.mp4inputs). Explicit-ois still honored as-is.3.
--replace(destructive in-place reencode)Encodes to a temp file in the destination directory, then atomically
os.replaces it over{stem}.mp4, deleting the original when the extension changes (ffmpeg can't read+write the same file in one pass). The temp file is cleaned up on failure/interrupt.--replaceis mutually exclusive with-o/--outputand unsupported for SLP inputs; clobbering an unrelated existing{stem}.mp4still requires--overwrite.Testing
test_run_subprocess_with_progress_*): feed a stand-in child emitting more stderr than any OS pipe buffer while streamingframe=N, run in a guard thread so a regression fails (not hangs) the test. Verified these deadlock under the old code and pass under the fix.--replace/.mp4default: dry-run path/messaging,-oconflict, SLP rejection, existing-target--overwriteguard, plus real in-place (.mp4) and extension-change (.mov→.mp4, original deleted) encodes (the two real encodes areskipif win32like the other slow video tests, but were verified manually on Windows here).reencodesuite green; lint clean. (One unrelated CSV-export test fails locally on a terminal-width rich-panel quirk — pre-existing onmain.)drone-clip.mov: 688/688 frames in ~3s.Design decisions
-nostats/-loglevel errorwould only reduce, not eliminate, the deadlock risk.frame=-emitting child without ffmpeg-specific flag insertion poisoning the command — no mocking/monkeypatching.--replace(not overloading--overwrite):--overwritekeeps its existing "clobber an existing output" meaning;--replaceis the distinct in-place operation.🤖 Generated with Claude Code