kopia lustrzana https://github.com/corrscope/corrscope
Fix freeze when rendering via GUI (#119)
Invoke GUI operations (progress dialog) in GUI thread, instead of calling slots from render thread.pull/357/head
rodzic
792f6ade91
commit
4c1b0426c6
|
@ -1,3 +1,4 @@
|
|||
import functools
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
@ -105,7 +106,7 @@ class MainWindow(qw.QMainWindow):
|
|||
self.actionExit.triggered.connect(qw.QApplication.closeAllWindows)
|
||||
|
||||
# Initialize CorrScope-thread attribute.
|
||||
self.corr_thread: Locked[Optional[CorrThread]] = Locked(None)
|
||||
self.corr_thread: Optional[CorrThread] = None
|
||||
|
||||
# Bind config to UI.
|
||||
self.load_cfg(cfg, cfg_path)
|
||||
|
@ -290,60 +291,60 @@ class MainWindow(qw.QMainWindow):
|
|||
def on_action_play(self):
|
||||
""" Launch CorrScope and ffplay. """
|
||||
error_msg = "Cannot play, another play/render is active"
|
||||
with self.corr_thread as t:
|
||||
if t is not None:
|
||||
self.corr_thread.unlock()
|
||||
qw.QMessageBox.critical(self, "Error", error_msg)
|
||||
return
|
||||
if self.corr_thread is not None:
|
||||
qw.QMessageBox.critical(self, "Error", error_msg)
|
||||
return
|
||||
|
||||
outputs = [FFplayOutputConfig()]
|
||||
self.play_thread(outputs, dlg=None)
|
||||
outputs = [FFplayOutputConfig()]
|
||||
self.play_thread(outputs, dlg=None)
|
||||
|
||||
def on_action_render(self):
|
||||
""" Get file name. Then show a progress dialog while rendering to file. """
|
||||
error_msg = "Cannot render to file, another play/render is active"
|
||||
with self.corr_thread as t:
|
||||
if t is not None:
|
||||
self.corr_thread.unlock()
|
||||
qw.QMessageBox.critical(self, "Error", error_msg)
|
||||
return
|
||||
if self.corr_thread is not None:
|
||||
qw.QMessageBox.critical(self, "Error", error_msg)
|
||||
return
|
||||
|
||||
video_path = os.path.join(self.cfg_dir, self.file_stem) + cli.VIDEO_NAME
|
||||
filters = ["MP4 files (*.mp4)", "All files (*)"]
|
||||
path = get_save_with_ext(
|
||||
self, "Render to Video", video_path, filters, cli.VIDEO_NAME
|
||||
)
|
||||
if path:
|
||||
name = str(path)
|
||||
# FIXME what if missing mp4?
|
||||
dlg = CorrProgressDialog(self, "Rendering video")
|
||||
video_path = os.path.join(self.cfg_dir, self.file_stem) + cli.VIDEO_NAME
|
||||
filters = ["MP4 files (*.mp4)", "All files (*)"]
|
||||
path = get_save_with_ext(
|
||||
self, "Render to Video", video_path, filters, cli.VIDEO_NAME
|
||||
)
|
||||
if path:
|
||||
name = str(path)
|
||||
dlg = CorrProgressDialog(self, "Rendering video")
|
||||
|
||||
outputs = [FFmpegOutputConfig(name)]
|
||||
self.play_thread(outputs, dlg)
|
||||
outputs = [FFmpegOutputConfig(name)]
|
||||
self.play_thread(outputs, dlg)
|
||||
|
||||
def play_thread(
|
||||
self, outputs: List[IOutputConfig], dlg: Optional["CorrProgressDialog"]
|
||||
):
|
||||
""" self.corr_thread MUST be locked. """
|
||||
arg = self._get_args(outputs)
|
||||
cfg = copy_config(self.model.cfg)
|
||||
t = self.corr_thread = CorrThread(cfg, arg)
|
||||
|
||||
if dlg:
|
||||
arg = attr.evolve(
|
||||
dlg.canceled.connect(t.abort)
|
||||
t.arg = attr.evolve(
|
||||
arg,
|
||||
on_begin=dlg.on_begin,
|
||||
progress=dlg.setValue,
|
||||
is_aborted=dlg.wasCanceled,
|
||||
on_end=dlg.reset, # TODO dlg.close
|
||||
on_begin=run_on_ui_thread(dlg.on_begin, (float, float)),
|
||||
progress=run_on_ui_thread(dlg.setValue, (int,)),
|
||||
is_aborted=t.is_aborted.get,
|
||||
on_end=run_on_ui_thread(dlg.reset, ()), # TODO dlg.close
|
||||
)
|
||||
|
||||
cfg = copy_config(self.model.cfg)
|
||||
t = self.corr_thread.obj = CorrThread(cfg, arg)
|
||||
t.finished.connect(self.on_play_thread_finished)
|
||||
t.error.connect(self.on_play_thread_error)
|
||||
t.ffmpeg_missing.connect(self.on_play_thread_ffmpeg_missing)
|
||||
t.start()
|
||||
|
||||
def _get_args(self, outputs: List[IOutputConfig]):
|
||||
arg = Arguments(cfg_dir=self.cfg_dir, outputs=outputs)
|
||||
return arg
|
||||
|
||||
def on_play_thread_finished(self):
|
||||
self.corr_thread.set(None)
|
||||
self.corr_thread = None
|
||||
|
||||
def on_play_thread_error(self, stack_trace: str):
|
||||
TracebackDialog(self).showMessage(stack_trace)
|
||||
|
@ -351,10 +352,6 @@ class MainWindow(qw.QMainWindow):
|
|||
def on_play_thread_ffmpeg_missing(self):
|
||||
DownloadFFmpegActivity(self)
|
||||
|
||||
def _get_args(self, outputs: List[IOutputConfig]):
|
||||
arg = Arguments(cfg_dir=self.cfg_dir, outputs=outputs)
|
||||
return arg
|
||||
|
||||
# File paths
|
||||
@property
|
||||
def cfg_dir(self) -> str:
|
||||
|
@ -381,25 +378,12 @@ class MainWindow(qw.QMainWindow):
|
|||
return self.model.cfg
|
||||
|
||||
|
||||
class ShortcutButton(qw.QPushButton):
|
||||
scoped_shortcut: QShortcut
|
||||
|
||||
def add_shortcut(self, scope: qw.QWidget, shortcut: str) -> None:
|
||||
""" Adds shortcut and tooltip. """
|
||||
keys = QKeySequence(shortcut, QKeySequence.PortableText)
|
||||
|
||||
self.scoped_shortcut = qw.QShortcut(keys, scope)
|
||||
self.scoped_shortcut.setContext(Qt.WidgetWithChildrenShortcut)
|
||||
self.scoped_shortcut.activated.connect(self.click)
|
||||
|
||||
self.setToolTip(keys.toString(QKeySequence.NativeText))
|
||||
|
||||
|
||||
class CorrThread(qc.QThread):
|
||||
def __init__(self, cfg: Config, arg: Arguments):
|
||||
qc.QThread.__init__(self)
|
||||
self.cfg = cfg
|
||||
self.arg = arg
|
||||
self.is_aborted = Locked(False)
|
||||
|
||||
def run(self) -> None:
|
||||
cfg = self.cfg
|
||||
|
@ -422,6 +406,12 @@ class CorrThread(qc.QThread):
|
|||
else:
|
||||
arg.on_end()
|
||||
|
||||
is_aborted: Locked[bool]
|
||||
|
||||
@qc.pyqtSlot()
|
||||
def abort(self):
|
||||
self.is_aborted.set(True)
|
||||
|
||||
error = qc.pyqtSignal(str)
|
||||
ffmpeg_missing = qc.pyqtSignal()
|
||||
|
||||
|
@ -442,11 +432,61 @@ class CorrProgressDialog(qw.QProgressDialog):
|
|||
# Close after CorrScope finishes.
|
||||
self.setAutoClose(True)
|
||||
|
||||
@qc.pyqtSlot(float, float)
|
||||
def on_begin(self, begin_time, end_time):
|
||||
self.setRange(int(round(begin_time)), int(round(end_time)))
|
||||
# self.setValue is called by CorrScope, on the first frame.
|
||||
|
||||
|
||||
T = TypeVar("T", bound=Callable)
|
||||
|
||||
|
||||
# *arg_types: type
|
||||
def run_on_ui_thread(bound_slot: T, types: Tuple[type, ...]) -> T:
|
||||
""" Runs an object's slot on the object's own thread.
|
||||
It's terrible code but it works (as long as the slot has no return value).
|
||||
"""
|
||||
qmo = qc.QMetaObject
|
||||
|
||||
# QObject *obj,
|
||||
obj = bound_slot.__self__
|
||||
|
||||
# const char *member,
|
||||
member = bound_slot.__name__
|
||||
|
||||
# Qt::ConnectionType type,
|
||||
# QGenericReturnArgument ret,
|
||||
# https://riverbankcomputing.com/pipermail/pyqt/2014-December/035223.html
|
||||
conn = Qt.QueuedConnection
|
||||
|
||||
@functools.wraps(bound_slot)
|
||||
def inner(*args):
|
||||
if len(types) != len(args):
|
||||
raise TypeError(f"len(types)={len(types)} != len(args)={len(args)}")
|
||||
|
||||
# https://www.qtcentre.org/threads/29156-Calling-a-slot-from-another-thread?p=137140#post137140
|
||||
# QMetaObject.invokeMethod(skypeThread, "startSkypeCall", Qt.QueuedConnection, QtCore.Q_ARG("QString", "someguy"))
|
||||
|
||||
_args = [qc.Q_ARG(typ, typ(arg)) for typ, arg in zip(types, args)]
|
||||
return qmo.invokeMethod(obj, member, conn, *_args)
|
||||
|
||||
return cast(T, inner)
|
||||
|
||||
|
||||
class ShortcutButton(qw.QPushButton):
|
||||
scoped_shortcut: QShortcut
|
||||
|
||||
def add_shortcut(self, scope: qw.QWidget, shortcut: str) -> None:
|
||||
""" Adds shortcut and tooltip. """
|
||||
keys = QKeySequence(shortcut, QKeySequence.PortableText)
|
||||
|
||||
self.scoped_shortcut = qw.QShortcut(keys, scope)
|
||||
self.scoped_shortcut.setContext(Qt.WidgetWithChildrenShortcut)
|
||||
self.scoped_shortcut.activated.connect(self.click)
|
||||
|
||||
self.setToolTip(keys.toString(QKeySequence.NativeText))
|
||||
|
||||
|
||||
def nrow_ncol_property(altered: str, unaltered: str) -> property:
|
||||
def get(self: "ConfigModel"):
|
||||
val = getattr(self.cfg.layout, altered)
|
||||
|
|
|
@ -37,7 +37,7 @@ class Locked(Generic[T]):
|
|||
return self.obj
|
||||
|
||||
def unlock(self):
|
||||
# FIXME does it work? i was not thinking clearly when i wrote this
|
||||
# FIXME don't use. What if we unlock, then someone else locks before we exit?
|
||||
if not self.skip_exit:
|
||||
self.skip_exit = True
|
||||
self.lock.unlock()
|
||||
|
@ -53,6 +53,10 @@ class Locked(Generic[T]):
|
|||
self.obj = value
|
||||
return value
|
||||
|
||||
def get(self) -> T:
|
||||
with self:
|
||||
return self.obj
|
||||
|
||||
|
||||
def get_save_with_ext(
|
||||
parent: QWidget,
|
||||
|
|
|
@ -103,9 +103,7 @@ class _FFmpegProcess:
|
|||
args, stdin=subprocess.PIPE, bufsize=bufsize, **kwargs
|
||||
)
|
||||
except FileNotFoundError as e:
|
||||
raise MissingFFmpegError(
|
||||
# FIXME REMOVE f'Class {obj_name(self)}: program {args[0]} is missing'
|
||||
)
|
||||
raise MissingFFmpegError()
|
||||
|
||||
def _generate_args(self) -> List[str]:
|
||||
return [arg for template in self.templates for arg in shlex.split(template)]
|
||||
|
|
Ładowanie…
Reference in New Issue