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:
@@ -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()
|
||||
Reference in New Issue
Block a user