diff --git a/.claude/settings.local.json b/.claude/settings.local.json index fbe8ed07..5de72aec 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -113,7 +113,8 @@ "Bash(ffprobe:*)", "Bash(gh api:*)", "Bash(git:*)", - "WebFetch(domain:docs.nvidia.com)" + "WebFetch(domain:docs.nvidia.com)", + "Bash(gh discussion:*)" ] } } diff --git a/CHANGES b/CHANGES index a32f16b9..407c2d1d 100644 --- a/CHANGES +++ b/CHANGES @@ -1,12 +1,19 @@ # Changelog +## Version 6.2.1 + +* Fixing #529 window geometry and menu anchoring issues when displays are powered off/on or reconfigured during use (thanks to wiznillyp) +* Fixing #734 startup crash when FFmpeg/FFprobe exits with non-zero code despite producing valid output (e.g. custom builds that crash during cleanup) (thanks to kliffgomel) +* Fixing #727 post-encode FFprobe failure when FFprobe crashes on cleanup but produces valid probe data (thanks to danycat201489-a11y) +* Fixing return from queue bug with FFmpeg nvenc av1 + ## Version 6.2.0 * Adding AV1 (NVENC) encoder for FFmpeg-based AV1 hardware encoding on NVIDIA GPUs (RTX 4000+) with quality-focused defaults including spatial/temporal AQ, lookahead, and multipass support * Adding #724 "exit" option to the After Conversion dropdown, which closes FastFlix after all queue items complete (thanks to jrff123) * Adding #731 OpenCL Support setting (Auto/Disable) with re-detection button in Application Locations settings (thanks to sks2012) * Adding favicon to root of repo so it shows up on fastflix.org (thanks to Balthazar) -* Adding encoding history feature with browsable history window, "Apply Last Used Settings" menu action, and startup opt-in prompt +* Adding #689 encoding history feature with browsable history window, "Apply Last Used Settings" menu action, and startup opt-in prompt (thanks to Augusto7743) * Adding FFmpeg 8.0+ version check on startup with option to download latest FFmpeg on Windows * Adding "Keep source format" option to Audio Normalize, which detects and uses the same audio codec and bitrate as the source video * Adding Audio Encoders tab in Settings to view and select which FFmpeg audio encoders appear in audio codec dropdowns diff --git a/fastflix/encoders/avc_x264/settings_panel.py b/fastflix/encoders/avc_x264/settings_panel.py index 43778fec..eb3f6f13 100644 --- a/fastflix/encoders/avc_x264/settings_panel.py +++ b/fastflix/encoders/avc_x264/settings_panel.py @@ -267,7 +267,7 @@ def update_video_encoder_settings(self): extra=self.ffmpeg_extras, tune=tune if tune.lower() != "default" else None, extra_both_passes=self.widgets.extra_both_passes.isChecked(), - bitrate_passes=int(self.widgets.bitrate_passes.currentText()), + bitrate_passes=int(self.widgets.bitrate_passes.currentText() or 1), aq_mode=self.widgets.aq_mode.currentText(), psy_rd=psy_rd_text if psy_rd_text else None, level=self.widgets.level.currentText(), diff --git a/fastflix/encoders/ffmpeg_av1_nvenc/settings_panel.py b/fastflix/encoders/ffmpeg_av1_nvenc/settings_panel.py index c3bf2439..c1900334 100644 --- a/fastflix/encoders/ffmpeg_av1_nvenc/settings_panel.py +++ b/fastflix/encoders/ffmpeg_av1_nvenc/settings_panel.py @@ -302,7 +302,7 @@ def update_video_encoder_settings(self): level=self.widgets.level.currentText() if self.widgets.level.currentIndex() != 0 else None, gpu=int(self.widgets.gpu.currentText() or -1) if self.widgets.gpu.currentIndex() != 0 else -1, b_ref_mode=self.widgets.b_ref_mode.currentText(), - aq_strength=int(self.widgets.aq_strength.currentText()), + aq_strength=int(self.widgets.aq_strength.currentText() or 8), tier=self.widgets.tier.currentText(), hw_accel=self.widgets.hw_accel.isChecked(), ) diff --git a/fastflix/encoders/hevc_x265/settings_panel.py b/fastflix/encoders/hevc_x265/settings_panel.py index 80ccdecd..cfbc682f 100644 --- a/fastflix/encoders/hevc_x265/settings_panel.py +++ b/fastflix/encoders/hevc_x265/settings_panel.py @@ -700,7 +700,7 @@ def update_video_encoder_settings(self): lossless=self.widgets.lossless.isChecked(), extra=self.ffmpeg_extras, extra_both_passes=self.widgets.extra_both_passes.isChecked(), - bitrate_passes=int(self.widgets.bitrate_passes.currentText()), + bitrate_passes=int(self.widgets.bitrate_passes.currentText() or 1), # gop_size=int(self.widgets.gop_size.currentText()) if self.widgets.gop_size.currentIndex() > 0 else 0, ) diff --git a/fastflix/flix.py b/fastflix/flix.py index 1c6dd41a..63feff89 100644 --- a/fastflix/flix.py +++ b/fastflix/flix.py @@ -158,9 +158,15 @@ def ffmpeg_configuration(app, config: Config, **_): """Extract the version and libraries available from the specified version of FFmpeg""" res = execute([f"{config.ffmpeg}", "-version"]) if res.returncode != 0: - logger.error(f"{config.ffmpeg} command stdout: {res.stdout}") - logger.error(f"{config.ffmpeg} command stderr: {res.stderr}") - raise FlixError(f'"{config.ffmpeg}" file not found or errored while executing. Return code {res.returncode}') + if not res.stdout or "ffmpeg version" not in res.stdout: + logger.error(f"{config.ffmpeg} command stdout: {res.stdout}") + logger.error(f"{config.ffmpeg} command stderr: {res.stderr}") + raise FlixError( + f'"{config.ffmpeg}" file not found or errored while executing. Return code {res.returncode}' + ) + logger.warning( + f"{config.ffmpeg} returned non-zero exit code {res.returncode} but produced valid output, continuing" + ) config = [] try: version = res.stdout.split(" ", 4)[2] @@ -187,7 +193,11 @@ def ffprobe_configuration(app, config: Config, **_): """Extract the version of ffprobe""" res = execute([f"{config.ffprobe}", "-version"]) if res.returncode != 0: - raise FlixError(f'"{config.ffprobe}" file not found') + if not res.stdout or "ffprobe version" not in res.stdout: + raise FlixError(f'"{config.ffprobe}" file not found') + logger.warning( + f"{config.ffprobe} returned non-zero exit code {res.returncode} but produced valid output, continuing" + ) try: version = res.stdout.split(" ", 4)[2] except (ValueError, IndexError): @@ -214,7 +224,9 @@ def probe(app: FastFlixApp, file: Path) -> Box: ] result = execute(command) if result.returncode != 0: - raise FlixError(f"Error code returned running FFprobe: {result.stdout} - {result.stderr}") + if not result.stdout or not result.stdout.strip().startswith("{"): + raise FlixError(f"Error code returned running FFprobe: {result.stdout} - {result.stderr}") + logger.warning(f"FFprobe returned non-zero exit code {result.returncode} but produced output, continuing") if result.stdout.strip() == "{}": raise FlixError(f"No output from FFprobe, not a known video type. stderr: {result.stderr}") diff --git a/fastflix/version.py b/fastflix/version.py index e69f10f6..5634478b 100644 --- a/fastflix/version.py +++ b/fastflix/version.py @@ -1,4 +1,4 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -__version__ = "6.2.0" +__version__ = "6.2.1" __author__ = "Chris Griffith" diff --git a/fastflix/widgets/container.py b/fastflix/widgets/container.py index dc8a8e0e..22431b0a 100644 --- a/fastflix/widgets/container.py +++ b/fastflix/widgets/container.py @@ -122,6 +122,43 @@ def __init__(self, app: FastFlixApp, **kwargs): # self.setWindowFlags(QtCore.Qt.WindowType.FramelessWindowHint) self.moveFlag = False + # Listen for display topology changes (monitors added/removed/reconfigured) + gui_app = QtGui.QGuiApplication.instance() + gui_app.screenAdded.connect(self._on_screen_change) + gui_app.screenRemoved.connect(self._on_screen_change) + gui_app.primaryScreenChanged.connect(self._on_screen_change) + # Track geometry/DPI changes on all current screens + for screen in gui_app.screens(): + self._connect_screen_signals(screen) + gui_app.screenAdded.connect(self._connect_screen_signals) + + def _connect_screen_signals(self, screen: QtGui.QScreen) -> None: + """Connect geometry/DPI change signals for a screen.""" + screen.geometryChanged.connect(self._on_screen_change) + screen.availableGeometryChanged.connect(self._on_screen_change) + screen.logicalDotsPerInchChanged.connect(self._on_screen_change) + + def _on_screen_change(self, *_args) -> None: + """Handle display topology or geometry changes by re-validating window bounds.""" + logger.debug("Screen change detected, re-validating window geometry") + # Use a short timer to coalesce rapid successive signals + if not hasattr(self, "_screen_change_timer"): + self._screen_change_timer = QtCore.QTimer(self) + self._screen_change_timer.setSingleShot(True) + self._screen_change_timer.setInterval(500) + self._screen_change_timer.timeout.connect(self._apply_screen_change) + self._screen_change_timer.start() + + def _apply_screen_change(self) -> None: + """Apply window adjustments after a screen change.""" + screen = self._current_screen() + if screen is None: + return + # Recalculate scale factors based on current window size + scaler.calculate_factors(self.width(), self.height()) + self._update_scaled_styles() + self.ensure_window_in_bounds() + def _current_screen(self) -> QtGui.QScreen: """Return the screen the window center is on, falling back to primary.""" screen = QtGui.QGuiApplication.screenAt(self.geometry().center()) diff --git a/fastflix/widgets/main.py b/fastflix/widgets/main.py index a0f55879..f417894c 100644 --- a/fastflix/widgets/main.py +++ b/fastflix/widgets/main.py @@ -1610,7 +1610,7 @@ def generate_output_filename(self): video_settings = None if video: video_settings = video.video_settings - encoder_settings = video.video_settings.video_encoder_settings + encoder_settings = getattr(video_settings, "video_encoder_settings", None) name = resolve_pre_encode_variables( gen_string,