Skip to content

v1.6.3

Latest

Choose a tag to compare

@TheBluWiz TheBluWiz released this 20 Sep 18:53

[1.6.3] - 2026-09-20

Fixed

  • muxm appeared to hang right after Loaded config: on large Matroska sources (and paused silently after every Matroska encode) — the console went quiet after ℹ️ Loaded config: ~/.muxmrc and stayed that way until Ctrl-C; the run log ended at → Caching stream metadata... and the failure-artifacts directory held only an empty probe_meta.err. The config import was never the blocker: the silent step is init_src_fps, which for a Matroska source called _fps_measure_avg_decimal (added in 1.6.2 to catch VFR sources whose r_frame_rate is a container timing base) — and that runs ffprobe -count_packets, which reads the entire file. Its comment claimed the pass "stays fast even on large files: ~0.1s measured on a 473MB capture"; that number came from a file already in the page cache and does not extrapolate — the count is I/O-bound and scales with file size. On a 29.5GB mkvmerge remux (Marty Supreme (2025).mkv, external APFS volume) it was still running after 6+ minutes, before the encode had even started, and nothing is logged until a step finishes, so it read as a deadlock. Confirmed by timing the exact ffprobe calls against the file: the metadata probe is 0.10s, the packet count is minutes. The same pass also ran on the output side of every Matroska encode's verify step (ffmpeg's muxer writes a per-stream DURATION tag, which is all the guard needs to proceed), a multi-GB full read before "Verifying output…" completes.

    Two changes, neither of which weakens the VFR detection where it matters. (1) Free tag check first — a Matroska video stream written by mkvmerge already carries NUMBER_OF_FRAMES and DURATION; init_src_fps computes the average from them (new _fps_tag_avg_decimal, arithmetic only, no I/O), and when it agrees with the nominal rate there is nothing to correct and the file is never read. That is every ordinary CFR movie (the reported file: 215328 ÷ 8980.972s = 23.976 = its 24000/1001). The tags are deliberately trusted only to prove agreement, never to override: a plain ffmpeg -c copy remux carries a stale NUMBER_OF_FRAMES over verbatim while rewriting DURATION (verified: a forced 999999 survived two remuxes), so a trimmed copy of a long film would otherwise "average" thousands of fps, be adopted by _fps_prefers_avg, and fail a good encode at the integrity guard (die 45). A disagreement, or absent tags, falls through to a real count. (2) Size gate — _fps_measure_avg_decimal now declines files over 2 GiB (returns nothing; callers keep the nominal r_frame_rate, exactly the pre-1.6.2 behavior), logging why to the run log. Large real-world media is essentially always CFR, which the nominal rate already gets right; the false positive this measurement exists for is a screen/game-capture recording, which is small. _fps_dur_to_secs (shared duration parsing) and _file_size_bytes (BSD/GNU stat probed separately, per the GNU stat -f hazard documented at _muxm_tier_is_trusted) were extracted to support this. Verified against the reported file with --dry-run and the same ~/.muxmrc/--hw-accel videotoolbox: Loaded config → Disk OK in the same second (previously 6+ minutes and counting).

    Note: the REPLACE_SOURCE prompt hang fixed below is a genuine but separate bug, found while investigating an earlier form of this report; it did not explain this one — the failing run had no source/output collision and never reached that prompt.

  • A .muxmrc-set REPLACE_SOURCE=1 could hang muxm indefinitely at the source-replacement confirmation prompt — REPLACE_SOURCE is a config-settable variable (CONFIG_TRACKED_VARS), but the --replace-source flag's "stdin must be a TTY" guard lives only in the CLI parsing arm, so a value arriving via ~/.muxmrc, /etc/.muxmrc, or ./.muxmrc reached the read -r _confirm prompt with no TTY guarantee at all. When the output path collided with the source and stdin was open but idle (a backgrounded run, or stdin piped from something that never writes or closes), that read blocked forever. Reproduced against the pre-fix code (an open, idle stdin pipe; killed only by an outer timeout, exit 124) and confirmed that muxm --help under the same .muxmrc returns immediately, so config sourcing itself was never the blocker. The prompt's read is now bounded — read -t 30 -r _confirm || _confirm="" — so an unanswered prompt resolves as a decline (die 11, "source replacement declined by user") after at most 30 seconds instead of hanging. The immediate-EOF case (stdin redirected from /dev/null) still declines instantly, exactly as the earlier M6 fix intended; the --replace-source flag's own parse-time TTY rejection is unchanged.

Tests

  • New cli-replace-source-timeout-bound (cli suite) — the existing cli-replace-source-eof test uses </dev/null, which hits EOF near-instantly and so passes identically with or without any timeout; nothing exercised the open-but-idle stdin shape that actually hung. The new test reproduces that shape (a process-substitution pipe nothing writes to) against a copy of muxm with the bound shortened from -t 30 to -t 1 (the same sed-copy idiom tools/perturb_check.sh uses), so it takes ~2s instead of 30s while still running the real read -t N line. It carries its own "exactly one line changed" anchor-drift guard and runs the target in the background under a 15s poll ceiling with a force-kill, so a regression fails the test instead of hanging the suite. Written for the harness's set -euo pipefail: the wait and diff | grep -c calls are ||-guarded (a bare wait; rc=$? would have silently aborted the whole run on the expected nonzero exit).
  • MUT-M6-EOF mutation anchor refreshed in tools/perturb_check.sh to the new read -t 30 -r _confirm || _confirm="" line; verified it still turns cli-replace-source-eof red (ERR-trap crash, exit 1) while the clean source passes.
  • containers suite: the AVI→MKV passthrough-fallback fixture was too short (1s) and failed deterministically — passthrough fallback: expected a derived .mkv output for the .avi source, none found. For a Matroska output muxm's fps-integrity guard measures the true average as packet count ÷ the video stream's tagged duration (ffprobe never populates avg_frame_rate for Matroska); over a 1s/25-frame clip a small fixed duration-tag offset (encoder priming) is a large enough fraction to skew that to ~23.05 fps against a true 24, tripping the guard's 1% tolerance — a false-positive "desync" that aborted the build (exit 45) after the fallback notice was already logged, so no output file was ever shipped. Reproduces in isolation, so not a parallel-run flake. Measured empirically: 1s (23.05 fps) and 2s (23.52 fps) still trip it; 3s and 5s do not. The fixture is now 3s. Test-only — the guard itself is unchanged, and it remains correct for real-world clip lengths.
  • unit-fps-no-full-read (unit suite, 8 assertions) — a PATH-shimmed ffprobe records any invocation, so the test asserts the cause (was the file read?) instead of a wall clock: agreeing tags on a Matroska source never trigger a count (run on a small file on purpose, so the size gate cannot mask a regression of the tag check); a tag-less or stale-tag source over the gate is not counted (using a sparse 3 GiB file, so no space or I/O); the same metadata on a small file is still counted (VFR detection preserved); a stale NUMBER_OF_FRAMES is never adopted; plus direct coverage of _fps_tag_avg_decimal. Unit suite 691 → 699.
  • Two new tools/perturb_check.sh anchors — MUT-FPS-TAGSKIP (tag check made always-true → every Matroska source is counted again) and MUT-FPS-SIZEGATE (ceiling raised out of reach). Each verified red-under-mutation and green-when-reverted by the real enforce gate.
  • Running the test suite no longer leaves an empty log.txt in the working directory — unit-dv-ffmpeg-wrap (_test_unit_dv_ffmpeg_wrap_mp4) passed the relative name log.txt as the LOG argument of _dv_ffmpeg_wrap_mp4, which redirects its (stubbed) ffmpeg's output there with >"$_log", so the shell created a zero-byte log.txt in whatever directory the harness was launched from (the repo root, in practice — where the log.txt line in .gitignore kept it out of git status without stopping it being created). The test asserts on the recorded ffmpeg argv and never reads the log, so the argument is now /dev/null. Test-only: muxm itself is unchanged, and the real call sites always pass $WORKDIR/… paths. Verified by deleting the file and re-running: the unit suite alone recreated it before the change; a full tests/run_parallel.sh after it (2646 pass / 0 fail / 1 skip, the baseline) leaves the repo root byte-for-byte identical to its pre-run listing.

Changed

  • Version bumped to 1.6.3. docs/muxm.1 regenerated via tools/gen-docs.sh (.TH version and release date); completions/muxm-completion.bash is unchanged. RELEASE_DATE set to 2026-09-20.