feat: MpvWidget with seek and AB-loop

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-06 12:12:53 +02:00
parent ba736fe532
commit ad1a4283e8
+63 -2
View File
@@ -1,9 +1,10 @@
import os import os
import subprocess import subprocess
import sys import sys
from PyQt6.QtCore import QThread, pyqtSignal import mpv
from PyQt6.QtCore import Qt, QThread, QTimer, pyqtSignal
from PyQt6.QtGui import QColor, QPainter, QPen from PyQt6.QtGui import QColor, QPainter, QPen
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget from PyQt6.QtWidgets import QApplication, QFrame, QMainWindow, QWidget
def build_export_path(folder: str, basename: str, counter: int) -> str: def build_export_path(folder: str, basename: str, counter: int) -> str:
@@ -117,6 +118,66 @@ class TimelineWidget(QWidget):
self.cursor_changed.emit(self._cursor) self.cursor_changed.emit(self._cursor)
class MpvWidget(QFrame):
file_loaded = pyqtSignal() # emitted (on Qt thread) when a file is ready
def __init__(self):
super().__init__()
self.setMinimumSize(640, 360)
self.setStyleSheet("background: black;")
# Required so Qt creates a real native window handle for mpv to embed into.
# Without these, mpv opens a separate window instead of embedding.
self.setAttribute(Qt.WidgetAttribute.WA_NativeWindow, True)
self.setAttribute(Qt.WidgetAttribute.WA_PaintOnScreen, True)
self._player = None
def _init_player(self):
if self._player is not None:
return
self._player = mpv.MPV(
wid=str(int(self.winId())),
keep_open=True,
pause=True,
)
# mpv fires events on its own thread; bounce to Qt thread via QTimer.
@self._player.event_callback("file-loaded")
def _on_file_loaded(event):
QTimer.singleShot(0, self.file_loaded.emit)
def load(self, path: str):
self._init_player()
self._player.play(path)
def seek(self, t: float):
if self._player:
self._player.pause = True
self._player.seek(t, "absolute")
def play_loop(self, a: float, b: float):
if self._player:
self._player["ab-loop-a"] = a
self._player["ab-loop-b"] = b
self._player.pause = False
def stop_loop(self):
if self._player:
# ab-loop-a/b are numeric properties — setting to "no" via dict
# accessor throws TypeError. Disable loop via ab_loop_count instead.
self._player.ab_loop_count = 0
self._player.pause = True
def get_duration(self) -> float:
if self._player:
d = self._player.duration
return d if d else 0.0
return 0.0
def closeEvent(self, event):
if self._player:
self._player.terminate()
super().closeEvent(event)
def main(): def main():
app = QApplication(sys.argv) app = QApplication(sys.argv)
win = MainWindow() win = MainWindow()