Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@
"Bash(ffprobe:*)",
"Bash(gh api:*)",
"Bash(git:*)",
"WebFetch(domain:docs.nvidia.com)"
"WebFetch(domain:docs.nvidia.com)",
"Bash(gh discussion:*)"
]
}
}
9 changes: 8 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion fastflix/encoders/avc_x264/settings_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion fastflix/encoders/ffmpeg_av1_nvenc/settings_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
Expand Down
2 changes: 1 addition & 1 deletion fastflix/encoders/hevc_x265/settings_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down
22 changes: 17 additions & 5 deletions fastflix/flix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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):
Expand All @@ -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}")
Expand Down
2 changes: 1 addition & 1 deletion fastflix/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__version__ = "6.2.0"
__version__ = "6.2.1"
__author__ = "Chris Griffith"
37 changes: 37 additions & 0 deletions fastflix/widgets/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
2 changes: 1 addition & 1 deletion fastflix/widgets/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading