fix: VAAPI filter chain, overwrite duplicates, export cursor stash, shutdown guards
- VAAPI: hwdownload before CPU filters, skip HW setup for image sequences - Delete old DB rows before overwrite re-insert to prevent duplicates - Stash cursor at export time so async completion uses correct position - Restore crop_center on marker click, fix falsy 0-value checks - Remove stale pending marker on export error - Guard double mpv termination in closeEvent - SnapPreviewWindow recursion guard for dock/follow moves Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -63,10 +63,12 @@ def build_ffmpeg_command(
|
|||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
# -ss before -i: fast input-seeking. Safe here because we always re-encode,
|
# -ss before -i: fast input-seeking. Safe here because we always re-encode,
|
||||||
# so there is no keyframe-alignment issue from pre-input seek.
|
# so there is no keyframe-alignment issue from pre-input seek.
|
||||||
|
# Image sequences always use libwebp, so skip HW encoder setup.
|
||||||
|
use_hw_vaapi = encoder == "h264_vaapi" and not image_sequence
|
||||||
cmd = ["ffmpeg", "-y"]
|
cmd = ["ffmpeg", "-y"]
|
||||||
|
|
||||||
# VAAPI needs a device for hardware context.
|
# VAAPI needs a device for hardware context.
|
||||||
if encoder == "h264_vaapi":
|
if use_hw_vaapi:
|
||||||
cmd += ["-hwaccel", "vaapi", "-hwaccel_output_format", "vaapi",
|
cmd += ["-hwaccel", "vaapi", "-hwaccel_output_format", "vaapi",
|
||||||
"-vaapi_device", "/dev/dri/renderD128"]
|
"-vaapi_device", "/dev/dri/renderD128"]
|
||||||
|
|
||||||
@@ -88,13 +90,12 @@ def build_ffmpeg_command(
|
|||||||
f"scale='if(lt(iw,ih),{short_side},-2)':'if(lt(iw,ih),-2,{short_side})':flags=lanczos"
|
f"scale='if(lt(iw,ih),{short_side},-2)':'if(lt(iw,ih),-2,{short_side})':flags=lanczos"
|
||||||
)
|
)
|
||||||
|
|
||||||
# VAAPI filters need upload to hw surface; crop/scale must happen on CPU
|
# VAAPI: decoded frames are GPU surfaces. CPU filters (crop/scale) need
|
||||||
# first, then upload back to VAAPI.
|
# hwdownload first, then re-upload for the HW encoder.
|
||||||
if encoder == "h264_vaapi":
|
if use_hw_vaapi:
|
||||||
if filters:
|
if filters:
|
||||||
filters.append("format=nv12")
|
filters.insert(0, "hwdownload")
|
||||||
filters.append("hwupload")
|
filters.insert(1, "format=nv12")
|
||||||
else:
|
|
||||||
filters.append("format=nv12")
|
filters.append("format=nv12")
|
||||||
filters.append("hwupload")
|
filters.append("hwupload")
|
||||||
|
|
||||||
@@ -1086,7 +1087,9 @@ class MpvWidget(QWidget):
|
|||||||
if self._render_ctx:
|
if self._render_ctx:
|
||||||
self._render_ctx.free()
|
self._render_ctx.free()
|
||||||
self._render_ctx = None
|
self._render_ctx = None
|
||||||
|
if self._player:
|
||||||
self._player.terminate()
|
self._player.terminate()
|
||||||
|
self._player = None
|
||||||
self._fbo = None
|
self._fbo = None
|
||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
|
|
||||||
@@ -1183,10 +1186,11 @@ class SnapPreviewWindow(QWidget):
|
|||||||
self._main_win = main_win
|
self._main_win = main_win
|
||||||
self._dock_edge: str | None = None # "left", "right", "top", "bottom" or None
|
self._dock_edge: str | None = None # "left", "right", "top", "bottom" or None
|
||||||
self._dock_offset: int = 0 # offset along the docked edge
|
self._dock_offset: int = 0 # offset along the docked edge
|
||||||
|
self._in_dock = False # recursion guard for move → dock → move
|
||||||
|
|
||||||
def moveEvent(self, event):
|
def moveEvent(self, event):
|
||||||
super().moveEvent(event)
|
super().moveEvent(event)
|
||||||
if not self._main_win.isVisible():
|
if self._in_dock or not self._main_win.isVisible():
|
||||||
return
|
return
|
||||||
mg = self._main_win.frameGeometry()
|
mg = self._main_win.frameGeometry()
|
||||||
pg = self.frameGeometry()
|
pg = self.frameGeometry()
|
||||||
@@ -1212,6 +1216,7 @@ class SnapPreviewWindow(QWidget):
|
|||||||
|
|
||||||
def _dock(self, edge: str, mg, pg) -> None:
|
def _dock(self, edge: str, mg, pg) -> None:
|
||||||
self._dock_edge = edge
|
self._dock_edge = edge
|
||||||
|
self._in_dock = True
|
||||||
if edge == "left":
|
if edge == "left":
|
||||||
x = mg.left() - pg.width()
|
x = mg.left() - pg.width()
|
||||||
self._dock_offset = pg.top() - mg.top()
|
self._dock_offset = pg.top() - mg.top()
|
||||||
@@ -1228,11 +1233,13 @@ class SnapPreviewWindow(QWidget):
|
|||||||
y = mg.bottom()
|
y = mg.bottom()
|
||||||
self._dock_offset = pg.left() - mg.left()
|
self._dock_offset = pg.left() - mg.left()
|
||||||
self.move(pg.left(), y)
|
self.move(pg.left(), y)
|
||||||
|
self._in_dock = False
|
||||||
|
|
||||||
def follow_main(self) -> None:
|
def follow_main(self) -> None:
|
||||||
"""Called by main window on move/resize to keep docked position."""
|
"""Called by main window on move/resize to keep docked position."""
|
||||||
if self._dock_edge is None:
|
if self._dock_edge is None:
|
||||||
return
|
return
|
||||||
|
self._in_dock = True
|
||||||
mg = self._main_win.frameGeometry()
|
mg = self._main_win.frameGeometry()
|
||||||
pw, ph = self.frameGeometry().width(), self.frameGeometry().height()
|
pw, ph = self.frameGeometry().width(), self.frameGeometry().height()
|
||||||
if self._dock_edge == "left":
|
if self._dock_edge == "left":
|
||||||
@@ -1243,6 +1250,7 @@ class SnapPreviewWindow(QWidget):
|
|||||||
self.move(mg.left() + self._dock_offset, mg.top() - ph)
|
self.move(mg.left() + self._dock_offset, mg.top() - ph)
|
||||||
elif self._dock_edge == "bottom":
|
elif self._dock_edge == "bottom":
|
||||||
self.move(mg.left() + self._dock_offset, mg.bottom())
|
self.move(mg.left() + self._dock_offset, mg.bottom())
|
||||||
|
self._in_dock = False
|
||||||
|
|
||||||
|
|
||||||
class PlaylistWidget(QListWidget):
|
class PlaylistWidget(QListWidget):
|
||||||
@@ -1975,10 +1983,16 @@ class MainWindow(QMainWindow):
|
|||||||
idx = self._cmb_format.findText(fmt)
|
idx = self._cmb_format.findText(fmt)
|
||||||
if idx >= 0:
|
if idx >= 0:
|
||||||
self._cmb_format.setCurrentIndex(idx)
|
self._cmb_format.setCurrentIndex(idx)
|
||||||
if meta["clip_count"]:
|
if meta["clip_count"] is not None:
|
||||||
self._spn_clips.setValue(meta["clip_count"])
|
self._spn_clips.setValue(meta["clip_count"])
|
||||||
if meta["spread"]:
|
if meta["spread"] is not None:
|
||||||
self._spn_spread.setValue(meta["spread"])
|
self._spn_spread.setValue(meta["spread"])
|
||||||
|
if meta["crop_center"] is not None:
|
||||||
|
self._crop_center = meta["crop_center"]
|
||||||
|
self._settings.setValue("crop_center", str(self._crop_center))
|
||||||
|
self._crop_bar.set_crop_center(self._crop_center)
|
||||||
|
if ratio != "Off":
|
||||||
|
self._mpv.set_crop_overlay(_RATIOS[ratio], self._crop_center)
|
||||||
self.statusBar().showMessage(
|
self.statusBar().showMessage(
|
||||||
f"Overwrite mode: {group_dir} ({n} clip{'s' if n != 1 else ''}) — export to replace", 5000
|
f"Overwrite mode: {group_dir} ({n} clip{'s' if n != 1 else ''}) — export to replace", 5000
|
||||||
)
|
)
|
||||||
@@ -2232,8 +2246,11 @@ class MainWindow(QMainWindow):
|
|||||||
base_center = self._crop_center
|
base_center = self._crop_center
|
||||||
|
|
||||||
if self._overwrite_path:
|
if self._overwrite_path:
|
||||||
# Group overwrite mode — re-export all sub-clips at this marker
|
# Group overwrite mode — re-export all sub-clips at this marker.
|
||||||
|
# Delete old DB rows first to avoid duplicates on re-insert.
|
||||||
group_paths = sorted(self._overwrite_group) if self._overwrite_group else [self._overwrite_path]
|
group_paths = sorted(self._overwrite_group) if self._overwrite_group else [self._overwrite_path]
|
||||||
|
for path in group_paths:
|
||||||
|
self._db.delete_by_output_path(path)
|
||||||
jobs = []
|
jobs = []
|
||||||
for i, path in enumerate(group_paths):
|
for i, path in enumerate(group_paths):
|
||||||
start = self._cursor + i * spread
|
start = self._cursor + i * spread
|
||||||
@@ -2275,7 +2292,9 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
short_side = self._spn_resize.value() or None
|
short_side = self._spn_resize.value() or None
|
||||||
|
|
||||||
# Stash export config for _on_clip_done DB writes
|
# Stash export config for _on_clip_done DB writes.
|
||||||
|
# Cursor is frozen here — user may move it during async export.
|
||||||
|
self._export_cursor = self._cursor
|
||||||
self._export_short_side = short_side
|
self._export_short_side = short_side
|
||||||
self._export_portrait = self._cmb_portrait.currentText()
|
self._export_portrait = self._cmb_portrait.currentText()
|
||||||
self._export_format = fmt
|
self._export_format = fmt
|
||||||
@@ -2317,7 +2336,7 @@ class MainWindow(QMainWindow):
|
|||||||
portrait = self._export_portrait if self._export_portrait != "Off" else ""
|
portrait = self._export_portrait if self._export_portrait != "Off" else ""
|
||||||
self._db.add(
|
self._db.add(
|
||||||
os.path.basename(self._file_path),
|
os.path.basename(self._file_path),
|
||||||
self._cursor,
|
self._export_cursor,
|
||||||
path,
|
path,
|
||||||
label=label,
|
label=label,
|
||||||
category=category,
|
category=category,
|
||||||
@@ -2362,6 +2381,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._btn_export.setEnabled(True)
|
self._btn_export.setEnabled(True)
|
||||||
self._btn_export.setText("Export")
|
self._btn_export.setText("Export")
|
||||||
self._btn_export.setStyleSheet("")
|
self._btn_export.setStyleSheet("")
|
||||||
|
self._refresh_markers() # remove stale pending marker
|
||||||
self.statusBar().showMessage(f"Export error: {msg}")
|
self.statusBar().showMessage(f"Export error: {msg}")
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
@@ -2374,7 +2394,9 @@ class MainWindow(QMainWindow):
|
|||||||
self._mpv._render_ctx.free()
|
self._mpv._render_ctx.free()
|
||||||
self._mpv._render_ctx = None
|
self._mpv._render_ctx = None
|
||||||
# Terminate the mpv player (joins its background threads).
|
# Terminate the mpv player (joins its background threads).
|
||||||
|
if self._mpv._player:
|
||||||
self._mpv._player.terminate()
|
self._mpv._player.terminate()
|
||||||
|
self._mpv._player = None
|
||||||
self._mpv._fbo = None
|
self._mpv._fbo = None
|
||||||
self._preview_win.close()
|
self._preview_win.close()
|
||||||
_log("Shutdown complete")
|
_log("Shutdown complete")
|
||||||
|
|||||||
Reference in New Issue
Block a user