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.
89 lines
2.9 KiB
Python
89 lines
2.9 KiB
Python
"""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()
|