"""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()