_seek_relative() was unconditionally calling player.pause() before
seeking. mpv's seek command doesn't itself change pause state, so
simply not forcing it is enough: paused stays paused, playing keeps
playing through the skip. (Frame-step still always pauses, correctly
-- that's for precisely inspecting a single frame, a different
use case from a coarse 5s nudge.)
Verified by calling _seek_relative() directly rather than simulating
the QShortcut keypress: a debug script isolated that PlayerWidget.seek()
itself works fine while playing, and QTest.keyClick against this
shortcut turned out to be unreliable in this headless setup even with
Qt's active-window state forced -- consistent with the flakiness
already noted in smoke_test_arrow_seek.py. The real key binding itself
was already confirmed with live xdotool keypresses in an earlier
session; this only needed to test the new state-preservation logic.
- Load File moved from a button into a new File menu.
- Timeline now sits directly under the video; the transport row
(frame-step/play/frame-step, centered as a group; mute pinned right)
moved below it, matching LosslessCut's layout instead of the previous
buttons-then-timeline order.
- Right-click on the video toggles play/pause.
The right-click implementation took two tries, worth recording: mpv
embeds via wid as its own native child window, which receives raw X11
input directly -- a real mouse click never reaches this QWidget's
mousePressEvent at all, even though a unit-style test that called the
handler directly passed and looked fine. Caught this by testing with
an actual xdotool click against a live running window instead of
trusting the synthetic test, which is what this project's smoke tests
have been doing throughout and what caught the bug here too. Fixed by
binding MBTN_RIGHT through mpv's own on_key_press input API instead,
which does receive the real clicks -- verified the same way, with a
real click on a live window, position/button-text actually changing.
Also fixed a genuinely flaky smoke test in the process: QShortcut's
active-window check races with real desktop window-manager focus,
especially with another instance of this same app already open
elsewhere (happened during this session). Switched that test to set
Qt's active-window state directly rather than relying on WM-level
activation, and independently confirmed the real key bindings with
live xdotool keypresses (both directions, plus end-of-file clamping).
- Left/Right arrow keys skip -/+5s (clamped to file bounds), pausing
first. QShortcut auto-repeats on its own while held, so no extra
timer/logic was needed for 'hold to keep skipping'.
- ffmpeg log panel starts hidden (view_menu default flipped to
unchecked) rather than visible by default.
- _load() now shows 'Loading <name>...' in the status bar and forces
one repaint before the blocking mpv load call, then 'Loaded <name>'
afterward -- player.load() itself stays synchronous, this just makes
the wait visible instead of a silent freeze.
Also loosened an unrelated flaky assertion in smoke_test_ui.py: hr-seek
was observed landing within ~2 frames of the nominal target across
runs (decode-order/B-frame timing, not a regression), and the old
34ms/1-frame tolerance was tighter than that natural variance.
- Clap detection moved from inline buttons to a Tools menu, plain labels
(no more '(whole file)' / '(from cursor)' parentheticals) -- status
messages moved to the window's status bar since the dedicated label
widget went away with the button row.
- ClickableSlider subclass: clicking anywhere along the seek track jumps
straight there instead of QSlider's default page-by-a-step behavior;
reposition-then-delegate-to-super keeps native drag behavior intact.
- Dragging now seeks live via sliderMoved instead of waiting for
sliderReleased.
- PlayerWidget accepts drag-and-drop of a local file to load it.
Verified with real Qt events (QMouseEvent clicks, a QDropEvent, a real
mouse click via xdotool opening the Tools menu on an actual running
window) rather than just calling the underlying methods directly.
Ports scripts/detect_claps.py (a librosa-based onset detector from an
earlier session) into clap_detection.py, shared by both the CLI script
and a new GUI feature: 'Detect All Claps' (whole-file scan) and
'Detect Next Clap' (from cursor, first result only), with results
shown as clickable markers on a new MarkerStrip widget above the seek
slider -- clicking one seeks the player to that exact timestamp.
Caught a real correctness bug while testing against the out-of-sync
fixture (not just the plain in-sync one): ffmpeg's default -vn audio
extraction silently drops the audio stream's own start_time and
renormalizes to t=0 -- which is exactly the offset this whole tool
exists to fix, so losing it meant detected timestamps pointed at the
wrong track's timing entirely. Fixed by probing the true start_time
via ffprobe and re-inserting it as explicit silence (adelay) before
analysis. Also dropped an earlier attempt at an ffmpeg -ss seek
optimization for the from-cursor case, since combining it with the
start_time fix meant two independent timestamp-origin corrections
stacked on each other -- correct in theory, too easy to get subtly
wrong in a tool where a silent wrong-timestamp bug is the worst
possible failure mode. Full-file decode + filter in Python is slower
but unambiguous.
Verified with a smoke test that drives the real MainWindow buttons,
confirms the detected marker lands near the fixture's known beep
(t=5.350s, allowing for onset-detection lag), and simulates an actual
QMouseEvent click on the marker to confirm click-to-seek works
end-to-end.
main.py wires up PlayerWidget with transport controls (play/pause,
frame-step buttons + ,/. shortcuts, seek slider), mute toggle, mark
video/audio buttons, live offset readout with manual override, and an
async export flow (QThread worker so ffmpeg doesn't block the UI) that
surfaces ffmpeg's stderr log without treating stream-copy warnings as
failures.
Verified two ways, not just by reading the code: a headless smoke test
that drives the real MainWindow/ExportWorker classes through a full
mark -> offset -> export cycle on the synthetic fixture, and a live
run (actual process, real X11 window, xdotool + import screenshot)
confirming the embedded mpv surface actually renders video inside the
Qt window rather than staying blank.
sync_logic.py: pure delay_ms computation with the documented sign
convention. Caught and fixed a sign error in my own test-fixture
ground-truth doc while writing the unit tests: the audio+350ms fixture
actually requires delay_ms=-350 (delay video), not +350, since the
audio is already the late one.
ffmpeg_export.py: ports ffdelay's two-branch command structure
(offset video input vs audio input depending on delay_ms sign) plus
its documented quirks (leading-zero float formatting, -ss/-t applied
to both inputs for clip mode, non-fatal stream-copy warnings surfaced
via stderr rather than swallowed).
Added an end-to-end integration test (scripts/test_export_roundtrip.py)
that independently re-detects event timing in the exported output via
ffmpeg scene-change/silence detection (not the same code path used to
build the export) and confirms both the positive- and negative-delay
branches bring drift down from ~350ms to under 1ms.
Confirms single-frame stepping is reliable: monotonic frame counter
across steps, and the synthetic clip's 1-frame flash event lands on
exactly one frame with clean neighbors. Also documents/works around
three environment quirks hit along the way: locally-built libmpv not
in ldconfig's cache, QApplication resetting LC_NUMERIC and breaking
libmpv's locale requirement, and the playback-restart event not firing
reliably after frame-step (switched to polling estimated_frame_number).