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).
142 lines
6.1 KiB
Python
142 lines
6.1 KiB
Python
"""Verifies the sync preview feature: building a short offset-corrected
|
|
clip and playing it in the Preview tab (Kdenlive-style project/clip
|
|
monitor tabs) instead of a popup dialog, plus the Sync markers panel
|
|
showing Mark Audio Event before Mark Video Event with a Detect All Claps
|
|
button alongside them.
|
|
"""
|
|
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")
|
|
|
|
|
|
def main():
|
|
qapp = QApplication(sys.argv)
|
|
win = app_main.MainWindow()
|
|
win.show()
|
|
qapp.processEvents()
|
|
|
|
# -- Marker panel order: Mark Audio Event before Mark Video Event, with
|
|
# Detect All Claps in the same panel.
|
|
marker_layout = win.mark_audio_button.parentWidget().layout()
|
|
widgets = [marker_layout.itemAt(i).widget() for i in range(marker_layout.count())]
|
|
widgets = [w for w in widgets if w is not None]
|
|
audio_idx = widgets.index(win.mark_audio_button)
|
|
video_idx = widgets.index(win.mark_video_button)
|
|
detect_idx = widgets.index(win.detect_all_button)
|
|
assert audio_idx < video_idx, "Mark Audio Event should come before Mark Video Event"
|
|
print(f"PASS: marker panel order is audio({audio_idx}) < video({video_idx}), detect button present at {detect_idx}")
|
|
|
|
# -- Two tabs: Source (main player) and Preview
|
|
assert win.video_tabs.count() == 2
|
|
assert win.video_tabs.tabText(0) == "Source"
|
|
assert win.video_tabs.tabText(1) == "Preview"
|
|
assert win.video_tabs.widget(0) is win.source_panel
|
|
assert win.video_tabs.widget(1) is win.preview_panel
|
|
print("PASS: video area has Source and Preview tabs")
|
|
|
|
win._load(CLIP)
|
|
qapp.processEvents()
|
|
time.sleep(0.2)
|
|
qapp.processEvents()
|
|
assert win.video_tabs.currentIndex() == 0, "loading a new source should land on the Source tab"
|
|
|
|
win.source_panel.player.seek(5.000)
|
|
qapp.processEvents()
|
|
win._mark_video()
|
|
|
|
win.source_panel.player.seek(5.350)
|
|
qapp.processEvents()
|
|
win._mark_audio()
|
|
|
|
assert win.preview_button.isEnabled()
|
|
win.preview_button.click()
|
|
qapp.processEvents()
|
|
assert not win.preview_button.isEnabled(), "preview button should disable while building"
|
|
pending_path = win._pending_preview_path
|
|
print(f"building preview clip at: {pending_path}")
|
|
|
|
deadline = time.time() + 20
|
|
while win.video_tabs.currentIndex() != 1 and time.time() < deadline:
|
|
qapp.processEvents()
|
|
time.sleep(0.05)
|
|
|
|
assert win.video_tabs.currentIndex() == 1, "finishing a preview should switch to the Preview tab"
|
|
assert win.preview_button.isEnabled()
|
|
assert win._preview_temp_path == pending_path
|
|
assert Path(win._preview_temp_path).exists()
|
|
print("PASS: preview build switched to the Preview tab automatically")
|
|
|
|
qapp.processEvents()
|
|
time.sleep(0.3)
|
|
qapp.processEvents()
|
|
|
|
duration = win.preview_panel.player.mpv.duration
|
|
print(f"preview clip duration: {duration:.3f}s (expected ~{win.PREVIEW_DURATION_SEC}s, clamped to file bounds)")
|
|
assert duration is not None and 0 < duration <= win.PREVIEW_DURATION_SEC + 0.5
|
|
assert win.preview_panel.player.mpv.pause is False, "preview should auto-play"
|
|
assert win.preview_panel.play_button.text() == "Pause"
|
|
|
|
# -- The Preview tab has its own functional timeline (not a static video
|
|
# + a single play button): its own seek slider, sized to the preview
|
|
# clip's own duration, that seeks the preview player independently of
|
|
# the Source tab's slider/player.
|
|
assert win.preview_panel.seek_slider is not win.source_panel.seek_slider
|
|
preview_duration_ms = win.preview_panel.seek_slider.maximum()
|
|
assert preview_duration_ms > 0
|
|
print(f"Preview tab's own seek slider range: 0..{preview_duration_ms}ms")
|
|
target_ms = int(preview_duration_ms * 0.7)
|
|
win.preview_panel.seek_slider.setValue(target_ms)
|
|
win.preview_panel.seek_slider.sliderMoved.emit(target_ms)
|
|
qapp.processEvents()
|
|
time.sleep(0.2)
|
|
qapp.processEvents()
|
|
preview_pos = win.preview_panel.player.get_position()
|
|
source_pos = win.source_panel.player.get_position()
|
|
print(f"dragged Preview tab's own slider to 70%: preview player now at {preview_pos:.3f}s, source player still at {source_pos:.3f}s")
|
|
assert abs(preview_pos - target_ms / 1000.0) < 0.5, "Preview tab's slider should seek the preview player"
|
|
print("PASS: Preview tab has its own independent, functional timeline")
|
|
|
|
# -- Switching back to Source shouldn't disturb the main player, and the
|
|
# preview player keeps running independently (two live mpv instances).
|
|
main_pos_before_switch = win.source_panel.player.get_position()
|
|
win.video_tabs.setCurrentIndex(0)
|
|
qapp.processEvents()
|
|
main_pos_after_switch = win.source_panel.player.get_position()
|
|
print(f"main player position: before switch={main_pos_before_switch:.3f}s after={main_pos_after_switch:.3f}s")
|
|
assert abs(main_pos_after_switch - main_pos_before_switch) < 0.1, "switching tabs shouldn't move the main player"
|
|
assert win.source_panel.player.mpv.pause is True, "main player should still be paused (untouched by preview building)"
|
|
print("PASS: switching tabs doesn't disturb the main player")
|
|
|
|
# -- Building a second preview swaps the file without leaking the first
|
|
# temp file.
|
|
first_preview_path = win._preview_temp_path
|
|
win.preview_button.click()
|
|
qapp.processEvents()
|
|
deadline = time.time() + 20
|
|
while win._pending_preview_path is not None and time.time() < deadline:
|
|
qapp.processEvents()
|
|
time.sleep(0.05)
|
|
time.sleep(0.2)
|
|
qapp.processEvents()
|
|
assert not Path(first_preview_path).exists(), "first preview temp file should be cleaned up after being replaced"
|
|
assert Path(win._preview_temp_path).exists()
|
|
print("PASS: rebuilding the preview cleans up the previous temp file")
|
|
|
|
win.close()
|
|
qapp.processEvents()
|
|
assert not Path(win._preview_temp_path).exists() if win._preview_temp_path else True
|
|
|
|
print("\nPREVIEW TAB SMOKE TEST PASSED")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|