Build the full UI (build order step 4)

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.
This commit is contained in:
ThePendulum
2026-07-26 00:53:51 +02:00
parent f634dda5bb
commit 660275bf02
2 changed files with 384 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
"""Launches the real MainWindow, loads the out-of-sync fixture, drives the
actual marker -> offset -> export flow through the same code the UI buttons
call (not a copy of the logic), and grabs screenshots so it can be visually
checked instead of just trusting the app "should" work.
"""
import sys
import time
from pathlib import Path
from PyQt6.QtWidgets import QApplication
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import main as app_main
ROOT = Path(__file__).resolve().parent.parent
CLIP = str(ROOT / "test_media" / "sync_test_OUTOFSYNC_audio+350ms.mp4")
OUT_DIR = ROOT / "test_media" / "seek_validation"
EXPORT_OUT = str(OUT_DIR / "_ui_smoke_export.mp4")
def main():
OUT_DIR.mkdir(exist_ok=True)
qapp = QApplication(sys.argv)
win = app_main.MainWindow()
win.show()
qapp.processEvents()
win._load(CLIP)
qapp.processEvents()
time.sleep(0.3)
qapp.processEvents()
print(f"loaded: duration slider range = {win.seek_slider.maximum()}ms")
win.player.seek(5.000)
qapp.processEvents()
win._mark_video()
print(f"video mark: {win.video_mark:.3f}s -> label: {win.video_mark_label.text()}")
win.player.seek(5.350)
qapp.processEvents()
win._mark_audio()
print(f"audio mark: {win.audio_mark:.3f}s -> label: {win.audio_mark_label.text()}")
print(f"offset label: {win.offset_label.text()}")
print(f"export button enabled: {win.export_button.isEnabled()}")
assert win.export_button.isEnabled()
# hr-seek lands on the nearest frame boundary (33.3ms steps @ 30fps), so
# marks won't hit 5.350 exactly -- check within one frame, not bit-exact.
assert abs(win.offset.delay_ms - (-350.0)) < 34
assert win.offset.target == "video"
win.grab().save(str(OUT_DIR / "ui_smoke_after_marking.png"))
# Drive export directly through the worker (skip the file-save dialog,
# which needs a real user) but through the same ExportWorker class the
# button uses.
done = {"ok": False, "err": None}
def on_ok(result):
done["ok"] = True
print("export finished OK, ffmpeg log:", result.stderr[:200])
def on_err(msg, stderr):
done["err"] = (msg, stderr)
worker = app_main.ExportWorker(win.input_path, win.offset.delay_ms, EXPORT_OUT)
worker.finished_ok.connect(on_ok)
worker.finished_err.connect(on_err)
worker.start()
deadline = time.time() + 15
while not done["ok"] and done["err"] is None and time.time() < deadline:
qapp.processEvents()
time.sleep(0.05)
if done["err"]:
raise RuntimeError(f"export failed: {done['err']}")
assert done["ok"], "export did not finish within timeout"
assert Path(EXPORT_OUT).exists()
print(f"exported file exists: {EXPORT_OUT}, size={Path(EXPORT_OUT).stat().st_size} bytes")
win.player.shutdown()
print("\nUI SMOKE TEST PASSED")
if __name__ == "__main__":
main()