Extracts a new PlayerPanel widget (video + its own seek slider/marker strip + its own centered frame-step/play/mute transport row) into player_panel.py. Both Source and Preview tabs now use one, so switching tabs switches a complete, independent monitor -- its own scrubber, its own play state, its own frame-stepping -- not just the video frame with a single shared play button underneath. main.py's global keyboard shortcuts (,/./space/left/right) now dispatch to whichever panel is active via a small _active_panel() helper, so they follow the same 'whichever monitor you're looking at' rule as the on-screen controls. MarkerStrip (clap detection) and the Mark Audio/Video/offset/export row stay outside the tabs, at the MainWindow level -- marking and exporting are inherently about the source file, not the preview clip, so only the timeline+transport moved, not the whole marking workflow. Updated every existing smoke test for the new win.source_panel.* / win.preview_panel.* structure (previously flat win.player, win.seek_slider, etc.), and added a new check to smoke_test_preview.py confirming the Preview tab's slider is a genuinely separate, functional widget -- dragging it seeks the preview player without moving the source player, which is the actual thing this change was for. Verified the full suite on the virtual Xvfb display, plus a live click-through (mark both events, build preview, confirm the Preview tab shows its own populated timeline/transport, not the source's).
95 lines
3.2 KiB
Python
95 lines
3.2 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.source_panel.seek_slider.maximum()}ms")
|
|
|
|
win.source_panel.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.source_panel.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 a nearby frame boundary (33.3ms steps @ 30fps), not
|
|
# necessarily the single nearest one (observed some run-to-run variance
|
|
# of a couple of frames, likely decode-order/B-frame timing rather than
|
|
# anything this smoke test is meant to pin down -- exact precision is
|
|
# covered separately by scripts/test_export_roundtrip.py's independent
|
|
# ffmpeg-based check). Just confirm we're in the right neighborhood and
|
|
# branch.
|
|
assert abs(win.offset.delay_ms - (-350.0)) < 100
|
|
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.source_panel.shutdown()
|
|
win.preview_panel.shutdown()
|
|
print("\nUI SMOKE TEST PASSED")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|