feat: MpvWidget with seek and AB-loop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user