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
119 changes: 119 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
{
"permissions": {
"allow": [
"WebSearch",
"WebFetch(domain:doc.qt.io)",
"WebFetch(domain:forum.qt.io)",
"WebFetch(domain:github.com)",
"Bash(uv run ruff check:*)",
"Bash(uv run:*)",
"Bash(python:*)",
"Bash(git log:*)",
"Bash(git show:*)",
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git blame:*)",
"Bash(git ls-files:*)",
"Bash(git ls-tree:*)",
"Bash(git rev-parse:*)",
"Bash(git reflog:*)",
"Bash(git describe:*)",
"Bash(git shortlog:*)",
"Bash(git stash list:*)",
"Bash(git cat-file:*)",
"Bash(git remote -v:*)",
"Bash(git config --get:*)",
"Bash(git config --list:*)",
"Bash(gh pr list:*)",
"Bash(gh pr view:*)",
"Bash(gh pr status:*)",
"Bash(gh pr checks:*)",
"Bash(gh pr diff:*)",
"Bash(gh issue list:*)",
"Bash(gh issue view:*)",
"Bash(gh issue status:*)",
"Bash(gh repo view:*)",
"Bash(gh repo list:*)",
"Bash(gh release list:*)",
"Bash(gh release view:*)",
"Bash(gh run list:*)",
"Bash(gh run view:*)",
"Bash(gh workflow list:*)",
"Bash(gh workflow view:*)",
"Bash(gh search:*)",
"Bash(gh status:*)",
"Bash(gh auth status:*)",
"Bash(gh label list:*)",
"Bash(gh gist list:*)",
"Bash(gh gist view:*)",
"Bash(gh cache list:*)",
"Bash(gh ruleset list:*)",
"Bash(gh ruleset view:*)",
"Bash(gh variable list:*)",
"Bash(gh secret list:*)",
"Bash(gh project list:*)",
"Bash(gh project view:*)",
"Bash(dir:*)",
"Bash(find:*)",
"Bash(findstr:*)",
"Bash(type:*)",
"Bash(where:*)",
"Bash(tree:*)",
"Bash(whoami:*)",
"Bash(hostname:*)",
"Bash(ver:*)",
"Bash(fc:*)",
"Bash(echo:*)",
"Bash(Get-Content:*)",
"Bash(Get-ChildItem:*)",
"Bash(Get-Item:*)",
"Bash(Get-ItemProperty:*)",
"Bash(Get-Location:*)",
"Bash(Get-Process:*)",
"Bash(Get-Command:*)",
"Bash(Get-Help:*)",
"Bash(Get-Date:*)",
"Bash(Get-Host:*)",
"Bash(Get-Module:*)",
"Bash(Get-Variable:*)",
"Bash(Get-Alias:*)",
"Bash(Get-Service:*)",
"Bash(Select-String:*)",
"Bash(Select-Object:*)",
"Bash(Where-Object:*)",
"Bash(Sort-Object:*)",
"Bash(Format-Table:*)",
"Bash(Format-List:*)",
"Bash(Measure-Object:*)",
"Bash(Compare-Object:*)",
"Bash(Test-Path:*)",
"Bash(Resolve-Path:*)",
"Bash(Split-Path:*)",
"Bash(Join-Path:*)",
"Bash(ls:*)",
"Bash(xargs:*)",
"WebFetch(domain:ffmpeg.org)",
"Bash(ffmpeg -h:*)",
"Bash(ffmpeg:*)",
"WebFetch(domain:gitlab.com)",
"WebFetch(domain:wiki.x266.mov)",
"WebFetch(domain:raw.githubusercontent.com)",
"Bash(grep:*)",
"WebFetch(domain:docs.rs)",
"WebFetch(domain:gist.github.com)",
"WebFetch(domain:manpages.debian.org)",
"WebFetch(domain:api.github.com)",
"WebFetch(domain:codecalamity.com)",
"WebFetch(domain:www.mail-archive.com)",
"WebFetch(domain:trac.ffmpeg.org)",
"WebFetch(domain:dev.to)",
"WebFetch(domain:www.ffmpeg.org)",
"WebFetch(domain:www.phoronix.com)",
"Bash(copy:*)",
"Bash(ffprobe:*)",
"Bash(gh api:*)",
"Bash(git:*)",
"WebFetch(domain:docs.nvidia.com)"
]
}
}
57 changes: 57 additions & 0 deletions .claude/skills/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
name: changelog
description: Update the CHANGES changelog file with new entries. MUST be consulted whenever adding, modifying, or removing entries in the CHANGES file, including when referencing GitHub issues.
user_invocable: true
trigger: Always read this skill BEFORE writing any changelog entry. Triggered by any task that involves updating the CHANGES file, adding a fix/feature note, or referencing a GitHub issue in the changelog.
---

# Changelog Skill

When updating the `CHANGES` file, follow these rules:

## Entry Format

Each entry is a single bullet point starting with `* `:

```
* {Verb} {description}
```

## Verbs and Ordering

Entries MUST use one of these four starting verbs, and MUST appear in this order within each version section:

1. **Adding** — new features
2. **Changing** — modifications to existing behavior
3. **Fixing** — bug fixes
4. **Removing** — removed features or deprecated items

## GitHub Issue Entries

- Entries that reference a GitHub issue include the issue number after the verb: `* Fixing #725 description...`
- Within each verb group, entries WITH issue numbers come FIRST, sorted by issue number ascending (smallest to largest)
- Entries WITHOUT issue numbers follow after

## Thanks Attribution

- When an entry references a GitHub issue, thank the issue author by their **GitHub display name** (not username)
- Look up the display name via `gh api users/{username} --jq '.name // .login'`
- Format: `(thanks to {display name})`
- If multiple people contributed (e.g., reporter and commenter with the fix), thank all of them
- The thanks attribution goes at the end of the entry

## Example

```
## Version 6.2.0

* Adding #731 OpenCL Support setting (thanks to sks2012)
* Adding FFmpeg 8.0+ version check on startup
* Adding "Keep source format" option to Audio Normalize
* Changing visual crop window to show rotated frame
* Changing -fps_mode to be used instead of deprecated -vsync
* Fixing #725 encoder detection to use ffmpeg -encoders (thanks to Davius and Generator)
* Fixing #730 subtitles tab missing on ARM (thanks to enaveso)
* Fixing cover extraction blocking video load
* Removing -strict experimental from SVT-AV1 encoders
```
28 changes: 28 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# Changelog

## 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 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
* Adding Data tab to profile settings with passthrough all or remove all options for data and attachment streams
* Adding clear current video X button next to source path and "Clear Current Video" option in File menu
* Adding rotation and flip buttons to the visual crop window, allowing users to change rotation (0/90/180/270) and toggle horizontal/vertical flip without leaving the crop view
* Changing visual crop window to show the video frame with rotation and flip applied, matching the final output so crop edges can be set intuitively in the rotated view
* Changing Copy encoder to use modern FFmpeg display_rotation, display_hflip, and display_vflip for lossless rotation and flip metadata instead of deprecated rotate metadata tag, with support for MP4, MOV, MKV, and M4V containers
* Changing non-copy encoder rotation handling to use FFmpeg's built-in auto-rotation instead of manual display_rotation overrides, which also properly handles source flips from the display matrix
* Changing -fps_mode to be used instead of deprecated -vsync for frame rate control
* Fixing #725 encoder detection to check `ffmpeg -encoders` output in addition to compilation flags, so encoders like VAAPI are shown even when the build flag is absent (thanks to Davius and Generator)
* Fixing #728 rigaya encoders (NVEncC, QSVEncC, VCEEncC) now pass --dolby-vision-rpu-prm crop=true when Dolby Vision RPU copy is enabled and a crop is applied (thanks to izzy697)
* Fixing #730 6.1.1 arm no subtitles tab with VideoToolBox (Apple M1 and above) HEVC & H264 (thanks to enaveso)
* Fixing page_update() busy-wait that could deadlock the GUI thread when called reentrantly
* Fixing shutdown-while-encoding bug where the worker would lose the shutdown intent after the current encode finished, requiring a forceful GUI kill instead of graceful shutdown
* Fixing visual crop window showing incorrect bounds and dimensions when user rotation is applied, by showing the frame in pre-rotation space where crop actually operates
* Fixing video crop and dimension detection for rotated videos where display matrix rotation was not found when other side data (e.g., HDR mastering display) preceded it
* Fixing cover extraction to not be during video load and blocking, but a background task
* Fixing 6.1.1 > 6.2.0 mixed queue saving bug (thanks to Norbert)
* Removing -strict experimental from SVT-AV1 encoders (no longer needed with FFmpeg 8+)

## Version 6.1.1

* Adding "Show completion popup message" (default off) and "Show error popup message" (default on) settings, replacing the old "Disable completion and error messages" toggle (thanks to Balthazar)
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

FastFlix is a Python GUI application for video encoding/transcoding using PySide6 (Qt6). It wraps FFmpeg and supports 25+ encoder backends including x264, x265, AV1 variants, VP9, VVC, and hardware encoders (NVIDIA NVEncC, Intel QSVEncC, AMD VCEEncC).

**Requirements:** Python 3.13+, FFmpeg 4.3+ (5.0+ recommended)
**Requirements:** Python 3.13+, FFmpeg 8.0+

## Build & Development Commands

Expand Down
101 changes: 99 additions & 2 deletions fastflix/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
import reusables
from PySide6 import QtGui, QtWidgets, QtCore

from fastflix.flix import ffmpeg_audio_encoders, ffmpeg_configuration, ffprobe_configuration, ffmpeg_opencl_support
from fastflix.flix import (
ffmpeg_audio_encoders,
ffmpeg_video_encoders,
ffmpeg_configuration,
ffprobe_configuration,
ffmpeg_opencl_support,
)
from fastflix.language import t
from fastflix.models.config import Config, MissingFF
from fastflix.models.fastflix import FastFlix
Expand Down Expand Up @@ -86,6 +92,7 @@ def init_encoders(app: FastFlixApp, **_):
from fastflix.encoders.gif import main as gif_plugin
from fastflix.encoders.gifski import main as gifski_plugin
from fastflix.encoders.ffmpeg_hevc_nvenc import main as nvenc_plugin
from fastflix.encoders.ffmpeg_av1_nvenc import main as ffmpeg_av1_nvenc_plugin
from fastflix.encoders.hevc_x265 import main as hevc_plugin
from fastflix.encoders.rav1e import main as rav1e_plugin
from fastflix.encoders.svt_av1 import main as svt_av1_plugin
Expand Down Expand Up @@ -115,6 +122,7 @@ def init_encoders(app: FastFlixApp, **_):
nvenc_plugin,
hevc_videotoolbox_plugin,
h264_videotoolbox_plugin,
ffmpeg_av1_nvenc_plugin,
av1_plugin,
rav1e_plugin,
svt_av1_plugin,
Expand Down Expand Up @@ -171,10 +179,23 @@ def init_encoders(app: FastFlixApp, **_):
# if "H.264/AVC" in app.fastflix.config.vceencc_encoders:
encoders.insert(encoders.index(avc_plugin), vceencc_avc_plugin)

# Mapping from requires values to search terms for ffmpeg -encoders output.
# Most requires values (e.g. "vaapi", "libx264") appear directly in encoder names,
# but some compilation flags don't match encoder names and need explicit mapping.
requires_to_encoder = {
"cuda-llvm": "nvenc",
}

def _encoder_available(requires: str) -> bool:
if requires in app.fastflix.ffmpeg_config:
return True
search_term = requires_to_encoder.get(requires, requires)
return any(search_term in enc for enc in (app.fastflix.video_encoders or []))

app.fastflix.encoders = {
encoder.name: encoder
for encoder in encoders
if (not getattr(encoder, "requires", None)) or encoder.requires in app.fastflix.ffmpeg_config or DEVMODE
if (not getattr(encoder, "requires", None)) or _encoder_available(encoder.requires) or DEVMODE
}


Expand All @@ -183,6 +204,58 @@ def init_fastflix_directories(app: FastFlixApp):
app.fastflix.log_path.mkdir(parents=True, exist_ok=True)


def _handle_ffmpeg_version_warning_windows(app: FastFlixApp, container: Container):
msg = QtWidgets.QMessageBox(container)
msg.setIcon(QtWidgets.QMessageBox.Warning)
msg.setWindowTitle(t("FFmpeg Version Warning"))
msg.setText(
t(
"Your FFmpeg (libavcodec {version}) is older than the required FFmpeg 8.0+ (libavcodec 62+)."
" Some features may not work correctly."
).format(version=app.fastflix.libavcodec_version)
)
cb = QtWidgets.QCheckBox(t("Don't show this warning again"))
msg.setCheckBox(cb)
download_btn = msg.addButton(t("Download Latest"), QtWidgets.QMessageBox.AcceptRole)
msg.addButton(t("Ignore"), QtWidgets.QMessageBox.RejectRole)
msg.exec()

if msg.clickedButton() == download_btn:
try:
container.status_bar.run_tasks(
[Task(t("Downloading FFmpeg"), grab_stable_ffmpeg)],
signal_task=True,
can_cancel=True,
)
ffmpeg_configuration(app)
except Exception:
logger.exception("Failed to download FFmpeg")

if cb.isChecked():
app.fastflix.config.suppress_ffmpeg_version_warning = True
app.fastflix.config.save()


def _handle_ffmpeg_version_warning_other(app: FastFlixApp):
msg = QtWidgets.QMessageBox()
msg.setIcon(QtWidgets.QMessageBox.Warning)
msg.setWindowTitle(t("FFmpeg Version Warning"))
msg.setText(
t(
"Your FFmpeg (libavcodec {version}) is older than the required FFmpeg 8.0+ (libavcodec 62+)."
" Please update FFmpeg. Visit https://ffmpeg.org/download.html"
).format(version=app.fastflix.libavcodec_version)
)
cb = QtWidgets.QCheckBox(t("Don't show this warning again"))
msg.setCheckBox(cb)
msg.addButton(t("OK"), QtWidgets.QMessageBox.AcceptRole)
msg.exec()

if cb.isChecked():
app.fastflix.config.suppress_ffmpeg_version_warning = True
app.fastflix.config.save()


def app_setup(
enable_scaling: bool = True,
portable_mode: bool = False,
Expand Down Expand Up @@ -347,13 +420,30 @@ def app_setup(
except Exception:
logger.exception("Failed to download HDR10+ tool")

if app.fastflix.config.enable_history is None:
history_choice = yes_no_message(
t("Would you like to enable encoding history?")
+ "\n\n"
+ t(
"This keeps a local record of your completed encodings, letting you review the settings used for any past video and quickly re-apply them to new ones."
)
+ "\n\n"
+ t("All data is stored locally on your computer. Nothing is sent to the internet."),
title=t("Enable Encoding History"),
)
if history_choice is not None:
app.fastflix.config.enable_history = history_choice
if history_choice:
container.rebuild_menu()

app.fastflix.config.save()

# Run startup tasks (FFmpeg config, encoder init) through status bar
startup_tasks = [
Task(t("Gather FFmpeg version"), ffmpeg_configuration),
Task(t("Gather FFprobe version"), ffprobe_configuration),
Task(t("Gather FFmpeg audio encoders"), ffmpeg_audio_encoders),
Task(t("Gather FFmpeg video encoders"), ffmpeg_video_encoders),
Task(t("Determine OpenCL Support"), ffmpeg_opencl_support),
Task(t("Initialize Encoders"), init_encoders),
]
Expand All @@ -366,6 +456,13 @@ def app_setup(
container.setEnabled(True)
return app

# Check FFmpeg version (libavcodec 62 = FFmpeg 8.x)
if not app.fastflix.config.suppress_ffmpeg_version_warning and 0 < app.fastflix.libavcodec_version < 62:
if reusables.win_based:
_handle_ffmpeg_version_warning_windows(app, container)
else:
_handle_ffmpeg_version_warning_other(app)

# Encoders are now populated — initialize the encoder UI
container.main.init_encoders_ui()

Expand Down
4 changes: 2 additions & 2 deletions fastflix/command_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,9 @@ def change_priority(
def _safe_log_put(self, msg):
"""Put message to log queue with timeout to prevent blocking if GUI is dead."""
try:
self.log_queue.put(msg, timeout=1.0)
self.log_queue.put(msg, timeout=0.1)
except Full:
pass # GUI likely dead, ignore
pass # GUI likely dead or log queue full, skip

def read_output(self):
with (
Expand Down
Loading
Loading