diff --git a/.github/workflows/build_linux.yaml b/.github/workflows/build_linux.yaml
index ecc1dbd7..10e67d10 100644
--- a/.github/workflows/build_linux.yaml
+++ b/.github/workflows/build_linux.yaml
@@ -27,7 +27,7 @@ jobs:
- name: Insatll requirements
run: |
python -m pip install --upgrade pip setuptools --ignore-installed
- python -m pip install --upgrade wheel
+ python -m pip install --upgrade wheel typing_extensions
python -m pip install -r requirements-build.txt
python -m pip install --upgrade pyqt5-tools
diff --git a/.github/workflows/build_linux_legacy.yaml b/.github/workflows/build_linux_legacy.yaml
index b53bca3b..50b6e6d0 100644
--- a/.github/workflows/build_linux_legacy.yaml
+++ b/.github/workflows/build_linux_legacy.yaml
@@ -27,7 +27,7 @@ jobs:
- name: Insatll requirements
run: |
python -m pip install --upgrade pip setuptools --ignore-installed
- python -m pip install --upgrade wheel
+ python -m pip install --upgrade wheel typing_extensions
python -m pip install -r requirements-build.txt
python -m pip install --upgrade pyqt5-tools
diff --git a/.github/workflows/build_mac.yaml b/.github/workflows/build_mac.yaml
index e2d02a8b..03f8d163 100644
--- a/.github/workflows/build_mac.yaml
+++ b/.github/workflows/build_mac.yaml
@@ -27,7 +27,7 @@ jobs:
- name: Insatll requirements
run: |
python -m pip install --upgrade pip setuptools --ignore-installed
- python -m pip install --upgrade wheel
+ python -m pip install --upgrade wheel typing_extensions
python -m pip install -r requirements-build.txt
- name: Grab iso-639 lists
diff --git a/.github/workflows/build_windows.yaml b/.github/workflows/build_windows.yaml
index 1059c0d5..1dfea57f 100644
--- a/.github/workflows/build_windows.yaml
+++ b/.github/workflows/build_windows.yaml
@@ -30,7 +30,7 @@ jobs:
shell: cmd
run: |
python -m pip install --upgrade pip setuptools --ignore-installed
- python -m pip install --upgrade pypiwin32 wheel
+ python -m pip install --upgrade pypiwin32 wheel typing_extensions
python -m pip install -r requirements-build.txt
- name: Grab iso-639 lists
diff --git a/CHANGES b/CHANGES
index 1d1d4941..e9914983 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,5 +1,29 @@
# Changelog
+## Version 4.2.0
+
+* Adding #109 NVENC HEVC support based on FFmpeg (thanks to Zeid164)
+* Adding NVEenC encoder for HEVC and AVC
+* Adding #166 More robust queue that is recoverable
+* Adding ability to extract HDR10+ metadata if hdr10plus_parser is detected on path
+* Adding #178 selector for number of autocrop positions throughout video (thanks to bmcassagne)
+* Adding Windows 10 notification for queue complete success
+* Adding #194 fast two pass encoding (thanks to Ugurtan)
+* Adding Confirm dialogue for cancel encode and replace currently working on video
+* Changing AVC defaults and recommendations for CRF to higher values
+* Changing VP9 to default to mkv instead of webm format to support more audio codecs
+* Fixing German translations (thanks to SMESH)
+* Fixing #171 Be able to select encoder before selecting video
+* Fixing #176 Unable to change queue order or delete task from queue since 4.1.0 (thanks to Etz)
+* Fixing #185 need to specify channel layout when downmixing (thanks to Ugurtan)
+* Fixing #187 cleaning up partial download of FFmpeg (thanks to Todd Wilkinson)
+* Fixing #190 add missing chromaloc parameter for x265 (thanks to Etz)
+* Fixing #209 Double spaces were removed in incoming filenames, causing no file found (thanks to stilicrafter)
+* Fixing that deinterlace detection could crash program due to CPython bug issue #43423 (thanks to macx)
+* Fixing that returning item back from queue of a different encoder type would crash Fastflix
+* Fixing HDR10 details to be track specific (thanks to Harybo)
+* Fixing returning from queue works with duplicated audio tracks
+
## Version 4.1.2
* Fixing #180 Minor UI glitch, custom bitrate retains "k" when edited from queue (thanks to Etz)
diff --git a/FastFlix_Nix_OneFile.spec b/FastFlix_Nix_OneFile.spec
index 3a748e70..0cd04122 100644
--- a/FastFlix_Nix_OneFile.spec
+++ b/FastFlix_Nix_OneFile.spec
@@ -11,7 +11,7 @@ for root, dirs, files in os.walk('fastflix'):
for file in files:
all_fastflix_files.append((os.path.join(root,file), root))
-all_imports = collect_submodules('pydantic') + ['dataclasses', 'colorsys']
+all_imports = collect_submodules('pydantic') + ['dataclasses', 'colorsys', 'typing_extensions']
with open("requirements-build.txt", "r") as reqs:
for line in reqs:
package = line.split("=")[0].split(">")[0].split("<")[0].replace('"', '').replace("'", '').strip()
@@ -41,7 +41,7 @@ exe = EXE(pyz,
debug=False,
bootloader_ignore_signals=False,
strip=False,
- upx=True,
+ upx=False,
upx_exclude=[],
runtime_tmpdir=None,
console=True , icon='fastflix/data/icon.ico')
diff --git a/FastFlix_Windows_Installer.spec b/FastFlix_Windows_Installer.spec
index dd4ae782..c1fc2d71 100644
--- a/FastFlix_Windows_Installer.spec
+++ b/FastFlix_Windows_Installer.spec
@@ -11,8 +11,7 @@ for root, dirs, files in os.walk('fastflix'):
for file in files:
all_fastflix_files.append((os.path.join(root,file), root))
-
-all_imports = collect_submodules('pydantic') + ['dataclasses', 'colorsys']
+all_imports = collect_submodules('pydantic') + ['dataclasses', 'colorsys', 'typing_extensions']
with open("requirements-build.txt", "r") as reqs:
for line in reqs:
package = line.split("=")[0].split(">")[0].split("<")[0].replace('"', '').replace("'", '').strip()
@@ -47,6 +46,6 @@ coll = COLLECT(exe,
a.zipfiles,
a.datas,
strip=False,
- upx=True,
+ upx=False,
upx_exclude=[],
name='FastFlix')
diff --git a/FastFlix_Windows_OneFile.spec b/FastFlix_Windows_OneFile.spec
index 88ce8eed..1634a192 100644
--- a/FastFlix_Windows_OneFile.spec
+++ b/FastFlix_Windows_OneFile.spec
@@ -11,7 +11,7 @@ for root, dirs, files in os.walk('fastflix'):
for file in files:
all_fastflix_files.append((os.path.join(root,file), root))
-all_imports = collect_submodules('pydantic') + ['dataclasses', 'colorsys']
+all_imports = collect_submodules('pydantic') + ['dataclasses', 'colorsys', 'typing_extensions']
with open("requirements-build.txt", "r") as reqs:
for line in reqs:
package = line.split("=")[0].split(">")[0].split("<")[0].replace('"', '').replace("'", '').strip()
@@ -41,7 +41,7 @@ exe = EXE(pyz,
debug=False,
bootloader_ignore_signals=False,
strip=False,
- upx=True,
+ upx=False,
upx_exclude=[],
runtime_tmpdir=None,
console=True , icon='fastflix\\data\\icon.ico')
diff --git a/README.md b/README.md
index 8533548d..7ab9aa66 100644
--- a/README.md
+++ b/README.md
@@ -16,24 +16,18 @@ Check out [the FastFlix github wiki](https://github.com/cdgriffith/FastFlix/wiki
# Encoders
- FastFlix supports the following encoders when their required libraries are found in FFmpeg:
+ FastFlix supports the following encoders if available:
-* HEVC (libx265)
-* AVC (libx264)
-* AV1 (librav1e)
-* AV1 (libaom-av1)
-* AV1 (libsvtav1)
-* VP9 (libvpx)
-* WEBP (libwebp)
-* GIF (gif)
+| Encoder | x265 | NVENC HEVC | [NVEncC HEVC](https://github.com/rigaya/NVEnc/releases) | x264 | rav1e | AOM AV1 | SVT AV1 | VP9 | WEBP | GIF |
+| --------- | ---- | ---------- | ----------- | ---- | ----- | ------- | ------- | --- | ---- | --- |
+| HDR10 | ✓ | | ✓ | | | | | ✓* | | |
+| HDR10+ | ✓ | | ✓ | | | | | | | |
+| Audio | ✓ | ✓ | ✓* | ✓ | ✓ | ✓ | ✓ | ✓ | | |
+| Subtitles | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | |
+| Covers | ✓ | ✓ | | ✓ | ✓ | ✓ | ✓ | | | |
+| bt.2020 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | |
-All of these are currently supported by [BtbN's Windows FFmpeg builds](https://github.com/BtbN/FFmpeg-Builds) which is the default FFmpeg downloaded.
-
-Most other builds do not have all these encoders available by default and may require custom compiling FFmpeg for a specific encoder.
-
-* [Windows FFmpeg (and more) auto builder](https://github.com/m-ab-s/media-autobuild_suite)
-* [Windows cross compile FFmpeg (build on linux)](https://github.com/rdp/ffmpeg-windows-build-helpers)
-* [FFmpeg compilation guide](https://trac.ffmpeg.org/wiki/CompilationGuide)
+`✓ - Full support | ✓* - Limited support`
# Releases
@@ -71,10 +65,12 @@ VP9 has limited support to copy some existing HDR10 metadata, usually from other
## HDR10+
-FastFlix supports using generated or [extracted JSON HDR10+ Metadata](https://github.com/cdgriffith/FastFlix/wiki/HDR10-Plus-Metadata-Extraction) with HEVC encodes via x265. However that is highly
-dependant on a FFmpeg version that has been compiled with x265 that has HDR10+ support. [BtbN's Windows FFmpeg builds](https://github.com/BtbN/FFmpeg-Builds)
+FastFlix supports using generated or [extracted JSON HDR10+ Metadata](https://github.com/cdgriffith/FastFlix/wiki/HDR10-Plus-Metadata-Extraction) with HEVC encodes via x265. However, that is highly
+dependent on a FFmpeg version that has been compiled with x265 that has HDR10+ support. [BtbN's Windows FFmpeg builds](https://github.com/BtbN/FFmpeg-Builds)
have this support as of 10/23/2020 and may require a [manual upgrade](https://github.com/cdgriffith/FastFlix/wiki/Updating-FFmpeg).
+If you add HDR10+ metadata file, make sure the encoding log does NOT contain the line `x265 [warning]: –dhdr10-info disabled. Enable HDR10_PLUS in cmake` or else it is unsupported.
+
## HLG
FastFlix (v4.0.2+) passes through HLG color transfer information to everything except webp and GIF.
@@ -83,6 +79,9 @@ FastFlix (v4.0.2+) passes through HLG color transfer information to everything e
FastFlix does not plan to support Dolby Vision's proprietary format at this time.
+# Support FastFlix
+
+Check out the different ways you can help [support FastFlix](https://github.com/cdgriffith/FastFlix/wiki/Support-FastFlix)!
# License
diff --git a/fastflix/application.py b/fastflix/application.py
index 50051c1c..6163692a 100644
--- a/fastflix/application.py
+++ b/fastflix/application.py
@@ -4,7 +4,7 @@
import coloredlogs
import reusables
-from qtpy import QtGui
+from qtpy import QtGui, QtWidgets, QtCore
from fastflix.flix import ffmpeg_audio_encoders, ffmpeg_configuration, ffprobe_configuration
from fastflix.language import t
@@ -22,6 +22,10 @@
def create_app():
+ if hasattr(QtCore.Qt, "AA_EnableHighDpiScaling"):
+ QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
+ if hasattr(QtCore.Qt, "AA_UseHighDpiPixmaps"):
+ QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
main_app = FastFlixApp(sys.argv)
main_app.setStyle("fusion")
main_app.setApplicationDisplayName("FastFlix")
@@ -49,24 +53,32 @@ def init_encoders(app: FastFlixApp, **_):
from fastflix.encoders.avc_x264 import main as avc_plugin
from fastflix.encoders.copy import main as copy_plugin
from fastflix.encoders.gif import main as gif_plugin
+ from fastflix.encoders.ffmpeg_hevc_nvenc import main as 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
from fastflix.encoders.vp9 import main as vp9_plugin
from fastflix.encoders.webp import main as webp_plugin
+ from fastflix.encoders.nvencc_hevc import main as nvencc_plugin
+ from fastflix.encoders.nvencc_avc import main as nvencc_avc_plugin
encoders = [
hevc_plugin,
- avc_plugin,
- gif_plugin,
- vp9_plugin,
- webp_plugin,
+ nvenc_plugin,
av1_plugin,
rav1e_plugin,
svt_av1_plugin,
+ avc_plugin,
+ vp9_plugin,
+ gif_plugin,
+ webp_plugin,
copy_plugin,
]
+ if app.fastflix.config.nvencc:
+ encoders.insert(1, nvencc_plugin)
+ encoders.insert(7, nvencc_avc_plugin)
+
app.fastflix.encoders = {
encoder.name: encoder
for encoder in encoders
@@ -94,9 +106,9 @@ def register_app():
logger.exception("Could not set application ID for Windows, please raise issue in github with above error")
-def start_app(worker_queue, status_queue, log_queue):
+def start_app(worker_queue, status_queue, log_queue, queue_list, queue_lock):
app = create_app()
- app.fastflix = FastFlix()
+ app.fastflix = FastFlix(queue=queue_list, queue_lock=queue_lock)
app.fastflix.log_queue = log_queue
app.fastflix.status_queue = status_queue
app.fastflix.worker_queue = worker_queue
diff --git a/fastflix/command_runner.py b/fastflix/command_runner.py
index b9d1f8e5..de8ec6e3 100644
--- a/fastflix/command_runner.py
+++ b/fastflix/command_runner.py
@@ -15,8 +15,6 @@
__all__ = ["BackgroundRunner"]
-white_detect = re.compile(r"^\s+")
-
class BackgroundRunner:
def __init__(self, log_queue):
@@ -106,6 +104,8 @@ def read_output(self):
err_excess = err_file.read()
logger.info(err_excess)
self.log_queue.put(err_excess)
+ if self.process.returncode is not None and self.process.returncode > 0:
+ self.error_detected = True
break
line = out_file.readline().rstrip()
if line:
@@ -156,7 +156,7 @@ def clean(self):
self.started_at = None
def kill(self, log=True):
- if self.process_two and self.process.poll() is None:
+ if self.process_two and self.process_two.poll() is None:
if log:
logger.info(f"Killing worker process {self.process_two.pid}")
try:
@@ -165,6 +165,7 @@ def kill(self, log=True):
except Exception as err:
if log:
logger.exception(f"Couldn't terminate process: {err}")
+
if self.process and self.process.poll() is None:
if log:
logger.info(f"Killing worker process {self.process.pid}")
@@ -193,23 +194,3 @@ def resume(self):
if not self.process:
return False
self.process.resume()
-
-
-# if __name__ == "__main__":
-# from queue import Queue
-#
-# logging.basicConfig(level=logging.DEBUG)
-# br = BackgroundRunner(Queue())
-# import shutil
-#
-# ffmpeg = shutil.which("ffmpeg")
-# br.start_piped_exec(
-# command_one=shlex.split(
-# rf'"{ffmpeg}" -loglevel panic -i C:\\Users\\Chris\\scoob_short.mkv -c:v copy -vbsf hevc_mp4toannexb -f hevc -'
-# ),
-# command_two=shlex.split(r'"C:\\Users\\Chris\\ffmpeg\\hdr10plus_parser.exe" --verify -'),
-# work_dir=r"C:\Users\Chris",
-# )
-# import time
-# time.sleep(1)
-# br.read_output()
diff --git a/fastflix/conversion_worker.py b/fastflix/conversion_worker.py
index ae6dc624..eeb77ba6 100644
--- a/fastflix/conversion_worker.py
+++ b/fastflix/conversion_worker.py
@@ -2,16 +2,28 @@
import logging
from pathlib import Path
from queue import Empty
+from typing import Optional
+from multiprocessing import Lock
import reusables
from appdirs import user_data_dir
+from pathvalidate import sanitize_filename
from fastflix.command_runner import BackgroundRunner
from fastflix.language import t
from fastflix.shared import file_date
+from fastflix.models.video import Video
+
logger = logging.getLogger("fastflix-core")
+log_path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "logs"
+after_done_path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "after_done_logs"
+
+queue_path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "queue.yaml"
+queue_lock_file = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "queue.lock"
+
+
CONTINUOUS = 0x80000000
SYSTEM_REQUIRED = 0x00000001
@@ -41,25 +53,91 @@ def allow_sleep_mode():
logger.debug("System has been allowed to enter sleep mode again")
+def get_next_video(queue_list, queue_lock) -> Optional[Video]:
+ with queue_lock:
+ logger.debug(f"Retrieving next video from {queue_list}")
+ for video in queue_list:
+ if (
+ not video.status.complete
+ and not video.status.success
+ and not video.status.cancelled
+ and not video.status.error
+ and not video.status.running
+ ):
+ logger.debug(f"Next video is {video.uuid} - {video.status}")
+ return video.copy()
+
+
+def set_status(
+ current_video: Video,
+ queue_list,
+ queue_lock,
+ complete=None,
+ success=None,
+ cancelled=None,
+ errored=None,
+ running=None,
+ next_command=False,
+ reset_commands=False,
+):
+ if not current_video:
+ return
+
+ with queue_lock:
+ for i, video in enumerate(queue_list):
+ if video.uuid == current_video.uuid:
+ video_pos = i
+ break
+ else:
+ logger.error(f"Can't find video {current_video.uuid} in queue to update its status: {queue_list}")
+ return
+
+ video_copy = queue_list.pop(video_pos)
+
+ if complete is not None:
+ video_copy.status.complete = complete
+ if cancelled is not None:
+ video_copy.status.cancelled = cancelled
+ if errored is not None:
+ video_copy.status.error = errored
+ if success is not None:
+ video_copy.status.success = success
+ if running is not None:
+ video_copy.status.running = running
+
+ if complete or cancelled or errored or success:
+ video_copy.status.running = False
+
+ if next_command:
+ video_copy.status.current_command += 1
+ if reset_commands:
+ video_copy.status.current_command = 0
+
+ queue_list.insert(video_pos, video_copy)
+
+
@reusables.log_exception(log="fastflix-core")
-def queue_worker(gui_proc, worker_queue, status_queue, log_queue):
+def queue_worker(gui_proc, worker_queue, status_queue, log_queue, queue_list, queue_lock: Lock):
runner = BackgroundRunner(log_queue=log_queue)
# Command looks like (video_uuid, command_uuid, command, work_dir)
after_done_command = ""
- commands_to_run = []
gui_died = False
currently_encoding = False
paused = False
- log_path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "logs"
- after_done_path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "after_done_logs"
+ video: Optional[Video] = None
def start_command():
nonlocal currently_encoding
- log_queue.put(f"CLEAR_WINDOW:{commands_to_run[0][0]}:{commands_to_run[0][1]}")
+ log_queue.put(
+ f"CLEAR_WINDOW:{video.uuid}:{video.video_settings.conversion_commands[video.status.current_command].uuid}"
+ )
reusables.remove_file_handlers(logger)
new_file_handler = reusables.get_file_handler(
- log_path / f"flix_conversion_{commands_to_run[0][4]}_{file_date()}.log",
+ log_path
+ / sanitize_filename(
+ f"flix_conversion_{video.video_settings.video_title or video.video_settings.output_path.stem}_{file_date()}.log"
+ ),
level=logging.DEBUG,
log_format="%(asctime)s - %(message)s",
encoding="utf-8",
@@ -68,11 +146,13 @@ def start_command():
prevent_sleep_mode()
currently_encoding = True
runner.start_exec(
- commands_to_run[0][2],
- work_dir=commands_to_run[0][3],
+ video.video_settings.conversion_commands[video.status.current_command].command,
+ work_dir=str(video.work_path),
)
+ set_status(video, queue_list=queue_list, queue_lock=queue_lock, running=True)
+ status_queue.put(("queue",))
- status_queue.put(("running", commands_to_run[0][0], commands_to_run[0][1], runner.started_at.isoformat()))
+ # status_queue.put(("running", commands_to_run[0][0], commands_to_run[0][1], runner.started_at.isoformat()))
while True:
if currently_encoding and not runner.is_alive():
@@ -82,8 +162,8 @@ def start_command():
# Stop working!
currently_encoding = False
- status_queue.put(("error", commands_to_run[0][0], commands_to_run[0][1]))
- commands_to_run = []
+ set_status(video, queue_list=queue_list, queue_lock=queue_lock, errored=True)
+ status_queue.put(("error",))
allow_sleep_mode()
if gui_died:
return
@@ -91,25 +171,35 @@ def start_command():
# Successfully encoded, do next one if it exists
# First check if the current video has more commands
- logger.info(t("Command has completed"))
- status_queue.put(("converted", commands_to_run[0][0], commands_to_run[0][1]))
- commands_to_run.pop(0)
- if commands_to_run:
- if not paused:
- logger.info(t("starting next command"))
- start_command()
- else:
- currently_encoding = False
- allow_sleep_mode()
- logger.debug(t("Queue has been paused"))
+ video.status.current_command += 1
+ log_queue.put("STOP_TIMER")
+
+ if len(video.video_settings.conversion_commands) > video.status.current_command:
+ logger.debug("About to run next command for this video")
+ set_status(video, queue_list=queue_list, queue_lock=queue_lock, next_command=True)
+ status_queue.put(("queue",))
+ start_command()
continue
else:
- logger.info(t("all conversions complete"))
- # Finished the queue
- # fastflix.current_encoding = None
+ logger.debug(f"{video.uuid} has been completed")
+ set_status(video, queue_list=queue_list, queue_lock=queue_lock, next_command=True, complete=True)
+ status_queue.put(("queue",))
+ video = None
+
+ if paused:
currently_encoding = False
- status_queue.put(("complete",))
allow_sleep_mode()
+ logger.debug(t("Queue has been paused"))
+ continue
+
+ if video := get_next_video(queue_list=queue_list, queue_lock=queue_lock):
+ start_command()
+ continue
+ else:
+ currently_encoding = False
+ allow_sleep_mode()
+ logger.info(t("all conversions complete"))
+ status_queue.put(("complete",))
if after_done_command:
logger.info(f"{t('Running after done command:')} {after_done_command}")
try:
@@ -142,36 +232,42 @@ def start_command():
# Request looks like (queue command, log_dir, (commands))
log_path = Path(request[1])
- for command in request[2]:
- if command not in commands_to_run:
- logger.debug(t(f"Adding command to the queue for {command[4]} - {command[2]}"))
- commands_to_run.append(command)
- # else:
- # logger.debug(t(f"Command already in queue: {command[1]}"))
- if not runner.is_alive() and not paused:
- logger.debug(t("No encoding is currently in process, starting encode"))
- start_command()
+ if not currently_encoding and not paused:
+ video = get_next_video(queue_list=queue_list, queue_lock=queue_lock)
+ if video:
+ start_command()
+
if request[0] == "cancel":
logger.debug(t("Cancel has been requested, killing encoding"))
runner.kill()
+ if video:
+ set_status(video, queue_list=queue_list, queue_lock=queue_lock, reset_commands=True, cancelled=True)
currently_encoding = False
allow_sleep_mode()
- status_queue.put(("cancelled", commands_to_run[0][0], commands_to_run[0][1]))
- commands_to_run = []
+ status_queue.put(("cancelled", video.uuid if video else ""))
+ log_queue.put("STOP_TIMER")
+ video = None
+
if request[0] == "pause queue":
logger.debug(t("Command worker received request to pause encoding after the current item completes"))
paused = True
+
if request[0] == "resume queue":
paused = False
logger.debug(t("Command worker received request to resume encoding"))
- if commands_to_run and not runner.is_alive():
- start_command()
+ if not currently_encoding:
+ if not video:
+ video = get_next_video(queue_list=queue_list, queue_lock=queue_lock)
+ if video:
+ start_command()
+
if request[0] == "set after done":
after_done_command = request[1]
if after_done_command:
logger.debug(f'{t("Setting after done command to:")} {after_done_command}')
else:
logger.debug(t("Removing after done command"))
+
if request[0] == "pause encode":
logger.debug(t("Command worker received request to pause current encode"))
try:
@@ -179,7 +275,7 @@ def start_command():
except Exception:
logger.exception("Could not pause command")
else:
- status_queue.put(("paused encode", commands_to_run[0][0], commands_to_run[0][1]))
+ status_queue.put(("paused encode",))
if request[0] == "resume encode":
logger.debug(t("Command worker received request to resume paused encode"))
try:
@@ -187,4 +283,4 @@ def start_command():
except Exception:
logger.exception("Could not resume command")
else:
- status_queue.put(("resumed encode", commands_to_run[0][0], commands_to_run[0][1]))
+ status_queue.put(("resumed encode",))
diff --git a/fastflix/data/encoders/icon_nvenc.png b/fastflix/data/encoders/icon_nvenc.png
new file mode 100644
index 00000000..7eb94ff9
Binary files /dev/null and b/fastflix/data/encoders/icon_nvenc.png differ
diff --git a/fastflix/data/encoders/icon_nvencc.png b/fastflix/data/encoders/icon_nvencc.png
new file mode 100644
index 00000000..2eccfd86
Binary files /dev/null and b/fastflix/data/encoders/icon_nvencc.png differ
diff --git a/fastflix/data/icons/undo-arrow.png b/fastflix/data/icons/undo-arrow.png
new file mode 100644
index 00000000..d4df03e7
Binary files /dev/null and b/fastflix/data/icons/undo-arrow.png differ
diff --git a/fastflix/data/languages.yaml b/fastflix/data/languages.yaml
index 38acfc4d..7052ed74 100644
--- a/fastflix/data/languages.yaml
+++ b/fastflix/data/languages.yaml
@@ -5,6 +5,13 @@
ita: 4 o 5 disattiveranno l'ottimizzazione della distorsione del tasso, con un impatto ancora maggiore sulla qualità.
spa: 4 o 5 desactivarán la optimización de la tasa de distorsión, lo que tendrá un impacto aún mayor en la calidad.
zho: 4或5会关闭rate distortion optimization,对质量的影响会更大。
+AQ Strength:
+ deu: Strärke des AQ
+ eng: AQ Strength
+ fra: AQ Force
+ ita: Forza AQ
+ spa: Fuerza de AQ
+ zho: AQ强度
About:
deu: Über
eng: About
@@ -54,6 +61,13 @@ Advanced:
ita: Avanzato
spa: Avanzado
zho: 高级
+Advanced settings are currently not saved in Profiles:
+ deu: Erweiterte Einstellungen werden derzeit nicht in Profilen gespeichert
+ eng: Advanced settings are currently not saved in Profiles
+ fra: Les paramètres avancés ne sont actuellement pas enregistrés dans les Profils
+ ita: Le impostazioni avanzate non sono attualmente salvate in Profili
+ spa: Los ajustes avanzados no se guardan actualmente en Perfiles
+ zho: 高级设置目前不会保存到方案中
After Conversion:
deu: Nach der Konvertierung
eng: After Conversion
@@ -90,7 +104,7 @@ Audio Tracks:
spa: Pistas de audio
zho: 音轨
Audio select language:
- deu: Audio Sprache auswählen
+ deu: Audio-Sprache auswählen
eng: Audio select language
fra: Audio choisir la langue
ita: Audio seleziona la lingua
@@ -111,7 +125,7 @@ Auto:
spa: Auto
zho: 自动
Auto Burn-in first forced or default subtitle track:
- deu: Auto Burn-in erste erzwungene oder Standard-Untertitelspur
+ deu: Auto Einbrennen der ersten erzwungenen oder Standard-Untertitelspur
eng: Auto Burn-in first forced or default subtitle track
fra: Auto Burn-in première piste de sous-titres forcée ou par défaut
ita: Auto Burn-in prima traccia sottotitoli forzata o predefinita
@@ -152,6 +166,20 @@ B Adapt:
ita: B Adattare
spa: B Adaptar
zho: B Adapt
+B Frames:
+ deu: B-Frames
+ eng: B Frames
+ fra: B Cadres
+ ita: B Frames
+ spa: Fotogramas B
+ zho: B型框架
+B Ref Mode:
+ deu: B-Ref-Modus
+ eng: B Ref Mode
+ fra: B Mode Ref
+ ita: Modalità B Ref
+ spa: Modo B Ref
+ zho: B参考模式
Bit Depth:
deu: Bit-Tiefe
eng: Bit Depth
@@ -173,6 +201,13 @@ Block Size:
ita: Dimensione del blocco
spa: Tamaño del bloque
zho: 块大小
+Both Passes:
+ deu: Beide Durchgänge
+ eng: Both Passes
+ fra: Les deux passages
+ ita: Entrambi i Pass
+ spa: Ambos pases
+ zho: 两遍均应用
Bottom:
deu: Unten
eng: Bottom
@@ -194,6 +229,13 @@ Break the video into rows to encode faster (lesser quality):
ita: Suddividere il video in righe per codificare più velocemente (qualità inferiore)
spa: Dividir el video en filas para codificar más rápido (menor calidad)
zho: 将视频按行分割,以更快地进行编码(质量较差)。
+Bufsize:
+ deu: Bufsize
+ eng: Bufsize
+ fra: Bufsize
+ ita: Bufsize
+ spa: Bufsize
+ zho: Bufsize
Build:
deu: Erstellen
eng: Build
@@ -215,6 +257,13 @@ CPU Used:
ita: CPU usata
spa: CPU utilizado
zho: CPU用量
+Calculate PSNR and SSIM and show in the encoder output:
+ deu: PSNR und SSIM berechnen und in der Kodierungsausgabe anzeigen
+ eng: Calculate PSNR and SSIM and show in the encoder output
+ fra: Calculer PSNR et SSIM et afficher dans la sortie du codeur
+ ita: Calcolare PSNR e SSIM e mostrare nell'output del codificatore
+ spa: Calcular PSNR y SSIM y mostrar en la salida del codificador
+ zho: 计算PSNR和SSIM,并显示在编码器输出中。
Cancel:
deu: Abbrechen
eng: Cancel
@@ -265,21 +314,21 @@ Cannot remove afterwards!:
spa: No se puede quitar después!
zho: 字幕内嵌之后无法去除!
Check for Newer Version of FastFlix:
- deu: Prüfen auf neuere Version von FastFlix
+ deu: Auf neuere Version von FastFlix überprüfen
eng: Check for Newer Version of FastFlix
fra: Consultez la nouvelle version de FastFlix
ita: Verifica la presenza di una versione più recente di FastFlix
spa: Busca la nueva versión de FastFlix
zho: 检查FastFlix更新
Clean Old Logs:
- deu: Alte Stämme reinigen
+ deu: Alte Protokolle löschen
eng: Clean Old Logs
fra: Nettoyer les vieilles bûches
ita: Pulire i vecchi tronchi
spa: Limpiar los viejos troncos
zho: 清理旧日志
Clear Completed:
- deu: Erledigt löschen
+ deu: Erledigte entfernen
eng: Clear Completed
fra: Clair Terminé
ita: Chiaro Completato
@@ -300,7 +349,7 @@ CodeCalamity UHD HDR Encoding Guide:
spa: CodeCalamity UHD Guía de codificación HDR
zho: CodeCalamity的UHD HDR编码指南(英文)
Color Primaries:
- deu: Farbe Primaries
+ deu: Grundfarbmodell
eng: Color Primaries
fra: Les couleurs primaires
ita: Primarie di colore
@@ -314,42 +363,42 @@ Color Space:
spa: Espacio de color
zho: 色彩空间
Color Transfer:
- deu: Farbe übertragen
+ deu: Übertragungsfunktion
eng: Color Transfer
fra: Transfert de couleur
ita: Trasferimento del colore
spa: Transferencia de color
zho: 色彩转换
Command has completed:
- deu: Befehl wurde beendet
+ deu: Befehl wurde abgeschlossen
eng: Command has completed
fra: Le commandement a terminé
ita: Il comando ha completato
spa: El comando ha completado
zho: 命令已完成
Command worker received request to pause current encode:
- deu: Command Worker hat Anforderung erhalten, die aktuelle Codierung zu pausieren
+ deu: Befehlsablauf hat Anfrage erhalten, die aktuelle Kodierung zu pausieren
eng: Command worker received request to pause current encode
fra: Un employé du commandement a reçu une demande de pause de l'encodage en cours
ita: Operatore di comando ha ricevuto la richiesta di mettere in pausa la codifica corrente
spa: El trabajador del comando recibió una solicitud para pausar la codificación actual
zho: 命令执行程序收到了暂停当前编码的请求
Command worker received request to pause encoding after the current item completes:
- deu: Command-Worker hat Anforderung erhalten, das Encoding nach Abschluss des aktuellen Elements zu pausieren
+ deu: Befehlsablauf hat Anfrage erhalten, das Encoding nach Abschluss des aktuellen Elements zu pausieren
eng: Command worker received request to pause encoding after the current item completes
fra: Un membre du personnel de commandement a reçu une demande de pause d'encodage après la fin de l'élément en cours
ita: L'operatore di comando ha ricevuto la richiesta di mettere in pausa la codifica dopo il completamento della voce corrente
spa: El trabajador del comando recibió la solicitud de pausar la codificación después de que el elemento actual complete
zho: 命令执行程序收到了在当前项目完成后暂停编码的请求
Command worker received request to resume encoding:
- deu: Befehls-Worker hat Anforderung erhalten, die Kodierung fortzusetzen
+ deu: Befehlsablauf hat Anfrage erhalten, die Kodierung fortzusetzen
eng: Command worker received request to resume encoding
fra: Le commandant a reçu une demande de reprise de l'encodage
ita: L'operatore di comando ha ricevuto la richiesta di riprendere la codifica
spa: El comandante recibió la solicitud de reanudar la codificación
zho: 命令执行程序收到了恢复编码的请求
Command worker received request to resume paused encode:
- deu: Befehls-Worker hat Anforderung erhalten, die pausierte Codierung fortzusetzen
+ deu: Befehlsablauf hat Anfrage erhalten, die pausierte Kodierung fortzusetzen
eng: Command worker received request to resume paused encode
fra: Un officier de commandement a reçu une demande de reprise de l'encodage en pause
ita: Operatore di comando ha ricevuto la richiesta di riprendere la pausa codificare
@@ -369,6 +418,13 @@ Config File:
ita: File di configurazione
spa: Archivo de configuración
zho: 配置文件
+Constant:
+ deu: Konstant
+ eng: Constant
+ fra: Constant
+ ita: Costante
+ spa: Constante
+ zho: 恒定
Conversion:
deu: Konvertierung
eng: Conversion
@@ -384,7 +440,7 @@ Conversion cancelled, delete incomplete file:
spa: Conversión cancelada, borrar archivo incompleto
zho: 转换已取消,删除不完整的文件
Conversion worker shutting down:
- deu: Konvertierungsarbeiter beim Herunterfahren
+ deu: Konvertierungsablauf fährt herunter
eng: Conversion worker shutting down
fra: Fermeture d'une entreprise de reconversion
ita: Operaio di conversione in chiusura
@@ -398,12 +454,12 @@ Convert:
spa: Convierte
zho: 转换
Convert BT2020 colorspace into bt709:
- deu: BT2020-Farbraum in bt709 konvertieren
- eng: Convert BT2020 colorspace into bt709
- fra: Convertir l'espace colorimétrique BT2020 en bt709
- ita: Convertire lo spazio di colore BT2020 in bt709
- spa: Convierte el espacio de color BT2020 en bt709
- zho: 将BT2020色彩空间转换为BT709
+ deu: BT.2020-Farbraum nach BT.709 konvertieren
+ eng: Convert BT.2020 colorspace into BT.709
+ fra: Convertir l'espace colorimétrique BT.2020 en BT.709
+ ita: Convertire lo spazio di colore BT.2020 in BT.709
+ spa: Convierte el espacio de color BT.2020 en BT.709
+ zho: 将BT.2020色彩空间转换为BT.709
Copy Chapters:
deu: Kapitel kopieren
eng: Copy Chapters
@@ -447,14 +503,14 @@ Copy Small Landscape Cover (no preview):
spa: Copia de la cubierta de un pequeño paisaje (sin vista previa)
zho: 复制横向小封面(无预览)
Copy all commands to the clipboard:
- deu: Kopiert alle Befehle in die Zwischenablage
+ deu: Kopiere alle Befehle in die Zwischenablage
eng: Copy all commands to the clipboard
fra: Copier toutes les commandes dans le presse-papiers
ita: Copiare tutti i comandi negli appunti
spa: Copia todos los comandos al portapapeles
zho: 将所有命令复制到剪贴板
Copy the chapter markers as is from incoming source.:
- deu: Kopieren Sie die Kapitelmarkierungen so wie sie sind aus der eingehenden Quelle.
+ deu: Unverändertes Kopieren der Kapitelmarkierungen aus der eingehenden Quelle.
eng: Copy the chapter markers as is from incoming source.
fra: Copiez les marqueurs de chapitre tels quels à partir de la source entrante.
ita: Copiare i marcatori dei capitoli come da sorgente in entrata.
@@ -482,7 +538,7 @@ Could not create / access work directory:
spa: No pudo crear / acceder al directorio de trabajo
zho: 无法创建/访问工作目录
Could not fix first subtitle track:
- deu: Konnte die erste Untertitelspur nicht fixieren
+ deu: Konnte die erste Untertitelspur nicht reparieren
eng: Could not fix first subtitle track
fra: Impossible de réparer la première piste de sous-titres
ita: Impossibile fissare la prima traccia dei sottotitoli
@@ -503,7 +559,7 @@ Could not load config file!:
spa: ¡No pude cargar el archivo de configuración!
zho: 无法加载配置文件!
Could not set language to:
- deu: Konnte die Sprache nicht auf
+ deu: Konnte die Sprache nicht ändern auf
eng: Could not set language to
fra: Impossible de régler la langue sur
ita: Non è stato possibile impostare la lingua su
@@ -537,6 +593,13 @@ Crop:
ita: Ritaglio
spa: Crop
zho: 裁剪
+Crop Detect Points:
+ deu: Feststellungspunkte fürs Cropping
+ eng: Crop Detect Points
+ fra: Points de détection des cultures
+ ita: Ritagliare i punti di rilevamento
+ spa: Puntos de detección de cultivos
+ zho: 作物检测点
Current Profile Settings:
deu: Aktuelle Profileinstellungen
eng: Current Profile Settings
@@ -551,6 +614,13 @@ Currently only works for image based subtitles.:
ita: Attualmente funziona solo per i sottotitoli basati su immagini.
spa: Actualmente sólo funciona para subtítulos basados en imágenes.
zho: 目前只适用于基于图像的字幕。
+Custom NVEncC options:
+ deu: angepasste NVEncC-Optionen
+ eng: Custom NVEncC options
+ fra: Options NVEncC personnalisées
+ ita: Opzioni NVEncC personalizzate
+ spa: Opciones personalizadas de NVEncC
+ zho: 自定义NVEncC选项
Custom ffmpeg options:
deu: Benutzerdefinierte ffmpeg-Optionen
eng: Custom ffmpeg options
@@ -559,14 +629,14 @@ Custom ffmpeg options:
spa: Opciones personalizadas de ffmpeg
zho: 自定义ffmpeg选项
Deblock:
- deu: Deblockieren
+ deu: Deblocking
eng: Deblock
fra: Deblock
ita: Deblock
spa: Deblock
zho: 去块
Default 4. This parameter has a quadratic effect on the amount of memory allocated:
- deu: Voreinstellung 4. Dieser Parameter hat einen quadratischen Effekt auf die Menge des zugewiesenen Speichers
+ deu: Voreinstellung 4. Die Menge des mit diesem Parameter zugewiesenen Speichers wächst quadratisch.
eng: Default 4. This parameter has a quadratic effect on the amount of memory allocated
fra: Défaut 4. Ce paramètre a un effet quadratique sur la quantité de mémoire allouée
ita: Predefinito 4. Questo parametro ha un effetto quadratico sulla quantità di memoria allocata
@@ -628,6 +698,13 @@ Denoise:
ita: Denoise
spa: Denoise
zho: 降噪
+Detect HDR10+:
+ deu: HDR10+ erkennen
+ eng: Detect HDR10+
+ fra: Détecter le HDR10+
+ ita: Rileva HDR10
+ spa: Detectar HDR10+
+ zho: 检测HDR10+
Detecting Interlace:
deu: Erkennen von Interlace
eng: Detecting Interlace
@@ -650,7 +727,7 @@ Disable update check on startup:
spa: Desactivar la comprobación de actualización al inicio
zho: 禁用启动时的更新检查
Disposition:
- deu: Disposition
+ deu: Verwendung
eng: Disposition
fra: Disposition
ita: Disposizione
@@ -664,7 +741,7 @@ Dither:
spa: Dither
zho: 抖动
Dither is an intentionally applied form of noise used to randomize quantization error,:
- deu: Dither ist eine absichtlich angewandte Form des Rauschens, die dazu dient, Quantisierungsfehler zu randomisieren,
+ deu: Dither dient dazu, sichtbare Quantisierungsfehler durch absichtliches Zufallsrauschen abzuschwächen.
eng: Dither is an intentionally applied form of noise used to randomize quantization error,
fra: Dither est une forme de bruit appliquée intentionnellement et utilisée pour randomiser l'erreur de quantification,
ita: Il dither è una forma di rumore applicata intenzionalmente usata per randomizzare l'errore di quantizzazione,
@@ -677,6 +754,13 @@ Download:
ita: Scaricare
spa: Descargar
zho: 下载
+Download Cancelled:
+ deu: Download abgebrochen
+ eng: Download Cancelled
+ fra: Téléchargement annulé
+ ita: Scaricamento annullato
+ spa: Descarga cancelada
+ zho: 下载取消
Download Newest FFmpeg:
deu: Neuestes FFmpeg herunterladen
eng: Download Newest FFmpeg
@@ -692,12 +776,19 @@ Downloading FFmpeg:
spa: Descargar FFmpeg
zho: 正在下载FFmpeg
Dual License:
- deu: Doppellizenz
+ deu: Doppellizenzierung
eng: Dual License
fra: Double licence
ita: Doppia Licenza
spa: Licencia doble
zho: 双重许可
+Enable VBV:
+ deu: VBV aktivieren
+ eng: Enable VBV
+ fra: Activer la VBV
+ ita: Attivare VBV
+ spa: Activar VBV
+ zho: 启用VBV
Enable row based multi-threading:
deu: Zeilenbasiertes Multithreading aktivieren
eng: Enable row based multi-threading
@@ -706,14 +797,14 @@ Enable row based multi-threading:
spa: Habilitar el multihilo basado en filas
zho: 启用基于行的多线程
Enable strong intra smoothing for 32x32 intra blocks.:
- deu: Aktivieren Sie die starke Intra-Glättung für 32x32-Intra-Blöcke.
+ deu: Aktivieren der starken Intra-Glättung für 32x32-Intra-Blöcke.
eng: Enable strong intra smoothing for 32x32 intra blocks.
fra: Permettre un lissage intra fort pour les blocs intra 32x32.
ita: Abilita una forte lisciatura intra per gli intrablocchi 32x32.
spa: Habilitar un fuerte alisamiento interno para los bloqueos internos de 32x32.
zho: Enable strong intra smoothing for 32x32 intra blocks.
Enable the yadif filter:
- deu: Aktivieren Sie den Yadif-Filter
+ deu: Aktivieren des Yadif-Filters
eng: Enable the yadif filter
fra: Activer le filtre yadif
ita: Attivare il filtro yadif
@@ -755,28 +846,28 @@ Enabling cover thumbnails on your system:
spa: Habilitando las miniaturas de la cubierta en su sistema
zho: 在系统上启用封面缩略图(英文)
Encoder:
- deu: Codierer
+ deu: Kodierer
eng: Encoder
fra: Encodeur
ita: Encoder
spa: Codificador
zho: 编码器
Encoder Output:
- deu: Encoder-Ausgabe
+ deu: Kodierer-Ausgabe
eng: Encoder Output
fra: Sortie de l'encodeur
ita: Uscita encoder
spa: Salida del codificador
zho: 编码器输出
Encoder Settings:
- deu: Encoder-Einstellung
+ deu: Kodierer-Einstellung
eng: Encoder Settings
fra: Réglage du codeur
ita: Impostazione dell'encoder
spa: Configuración del codificador
zho: 编码器设置
Encoding Queue:
- deu: Kodierungswarteschlange
+ deu: Kodierer-Warteschlange
eng: Encoding Queue
fra: File d'attente d'encodage
ita: Coda di codifica
@@ -803,6 +894,13 @@ Encoding complete:
ita: Codifica completa
spa: Codificación completa
zho: 编码完成
+Encoding errored:
+ deu: Error beim Kodieren
+ eng: Encoding errored
+ fra: Encodage erroné
+ ita: Codifica errata
+ spa: ''
+ zho: 编码错误
End:
deu: Beenden
eng: End
@@ -811,7 +909,7 @@ End:
spa: Fin
zho: 结束
Enforce an encode profile:
- deu: Erzwingen eines Kodierprofils
+ deu: Erzwingen eines Kodiererprofils
eng: Enforce an encode profile
fra: Faire appliquer un profil codé
ita: Applicare un profilo di codifica
@@ -846,7 +944,7 @@ Estimated file size based on bitrate:
spa: Tamaño estimado del archivo basado en la tasa de bits
zho: 基于比特率的估计文件大小
Estimated time left for current command:
- deu: Geschätzte verbleibende Zeit für den aktuellen Befehl
+ deu: Geschätzte Restzeit für den aktuellen Befehl
eng: Estimated time left for current command
fra: Estimation du temps restant pour le commandement en cours
ita: Tempo stimato rimasto per il comando corrente
@@ -880,43 +978,78 @@ Extra x265 params in opt=1:opt2=0 format:
ita: Parametri extra x265 nel formato opt=1:opt2=0
spa: Parámetros extra x265 en formato opt=1:opt2=0
zho: 额外的x265参数,格式为opt=1:opt2=0
+Extract:
+ deu: Auszug
+ eng: Extract
+ fra: Extrait
+ ita: Estratto
+ spa: Extracto
+ zho: 提取
+Extract HDR10+:
+ deu: HDR10+ extrahieren
+ eng: Extract HDR10+
+ fra: Extrait HDR10+
+ ita: Estrarre HDR10
+ spa: Extraer HDR10+
+ zho: 提取HDR10+
Extract covers:
- deu: Extrahieren deckt ab
+ deu: Covers extrahieren
eng: Extract covers
fra: Extrait couvre
ita: Coperture per l'estrazione
spa: Extraer las cubiertas
zho: 提取封面
+Extracted subtitles successfully:
+ deu: Untertitel erfolgreich extrahiert
+ eng: Extracted subtitles successfully
+ fra: Sous-titres extraits avec succès
+ ita: Sottotitoli estratti con successo
+ spa: Subtítulos extraídos con éxito
+ zho: 成功提取字幕
+Extracting HDR10+ metadata:
+ deu: HDR10+-Metadaten extrahieren
+ eng: Extracting HDR10+ metadata
+ fra: Extraction des métadonnées HDR10+.
+ ita: Estrarre i metadati HDR10
+ spa: Extracción de metadatos HDR10+
+ zho: 提取HDR10+元数据
+Extracting subtitles to:
+ deu: Extrahieren von Untertiteln auf
+ eng: Extracting subtitles to
+ fra: Extraction des sous-titres de
+ ita: Estrazione dei sottotitoli a
+ spa: Extracción de subtítulos a
+ zho: 提取字幕到
FFMPEG AV1 Encoding Guide:
- deu: FFMPEG AV1-Kodierungsleitfaden
+ deu: FFMPEG AV1 Kodierungsanleitung
eng: FFMPEG AV1 Encoding Guide
fra: Guide d'encodage FFMPEG AV1
ita: Guida alla codifica FFMPEG AV1
spa: Guía de codificación del FFMPEG AV1
zho: FFMPEG AV1编码指南(英文)
FFMPEG AVC / H.264 Encoding Guide:
- deu: FFMPEG AVC / H.264 Kodierungsleitfaden
+ deu: FFMPEG AVC / H.264 Kodierungsanleitung
eng: FFMPEG AVC / H.264 Encoding Guide
fra: FFMPEG AVC / Guide d'encodage H.264
ita: Guida alla codifica FFMPEG AVC / H.264
spa: Guía de codificación FFMPEG AVC / H.264
zho: FFMPEG AVC / H.264 编码指南(英文)
FFMPEG HEVC / H.265 Encoding Guide:
- deu: FFMPEG HEVC / H.265 Kodierungsleitfaden
+ deu: FFMPEG HEVC / H.265 Kodierungsanleitung
eng: FFMPEG HEVC / H.265 Encoding Guide
fra: FFMPEG HEVC / Guide d'encodage H.265
ita: Guida alla codifica FFMPEG HEVC / H.265
spa: Guía de codificación FFMPEG HEVC / H.265
zho: FFMPEG HEVC / H.265编码指南(英文)
FFMPEG VP9 Encoding Guide:
- deu: FFMPEG VP9-Encoding-Anleitung
+ deu: FFMPEG VP9 Kodierungsanleitung
eng: FFMPEG VP9 Encoding Guide
fra: FFMPEG Guide d'encodage VP9
ita: Guida alla codifica FFMPEG VP9
spa: Guía de codificación del FFMPEG VP9
zho: FFMPEG VP9编码指南(英文)
FFmpeg updated - Please restart FastFlix:
- deu: FFmpeg aktualisiert - Bitte starten Sie FastFlix neu
+ deu: FFmpeg aktualisiert - Bitte FastFlix neustarten
eng: FFmpeg updated - Please restart FastFlix
fra: FFmpeg mis à jour - Veuillez redémarrer FastFlix
ita: FFmpeg aggiornato - Si prega di riavviare FastFlix
@@ -929,22 +1062,36 @@ FPS:
ita: FPS
spa: FPS
zho: FPS
+Fast first pass:
+ deu: schneller erster Durchlauf
+ eng: Fast first pass
+ fra: Première passe rapide
+ ita: Primo passaggio veloce
+ spa: Primera pasada rápida
+ zho: 快速的第一道
+File:
+ deu: Datei
+ eng: File
+ fra: Fichier
+ ita: Archivio
+ spa: Archivo
+ zho: 文件
Flat UI:
- deu: Flaches UI
- eng: Flat UI
- fra: Assurance-chômage à plat
- ita: IU piatta
- spa: UI plana
- zho: 扁平化UI
+ deu: Flaches Design der GUI
+ eng: Flat GUI design
+ fra: interface graphique à plat
+ ita: GUI piatta
+ spa: GUI plana
+ zho: 扁平化GUI
For lossless, this is a size/speed tradeoff.:
- deu: Bei verlustfreier Encodierung ist dies ein Kompromiss zwischen Größe und Geschwindigkeit.
+ deu: Bei verlustfreier Kodierung ist dies ein Kompromiss zwischen Größe und Geschwindigkeit.
eng: For lossless, this is a size/speed tradeoff.
fra: Pour les moins fortunés, il s'agit d'un compromis taille/vitesse.
ita: Per i lossless, questo è un compromesso tra dimensioni e velocità.
spa: Para los que no tienen pérdidas, esto es un intercambio de tamaño/velocidad.
zho: 在无损编码时,此选项在大小与速度之间权衡。
For lossy, this is a quality/speed tradeoff.:
- deu: Für verlustbehaftet ist dies ein Kompromiss zwischen Qualität und Geschwindigkeit.
+ deu: Für verlustbehafteter Kodierung ist dies ein Kompromiss zwischen Qualität und Geschwindigkeit.
eng: For lossy, this is a quality/speed tradeoff.
fra: Pour les perdants, il s'agit d'un compromis qualité/vitesse.
ita: Per le perdite, si tratta di un compromesso qualità/velocità.
@@ -971,22 +1118,36 @@ Frames Per Second:
ita: Cornici al secondo
spa: Cuadros por segundo
zho: 每秒帧数
+GPU:
+ deu: GPU
+ eng: GPU
+ fra: GPU
+ ita: GPU
+ spa: GPU
+ zho: GPU
+GUI Logging Level:
+ deu: GUI-Protokollierungsebene
+ eng: GUI Logging Level
+ fra: Niveau d'exploitation forestière
+ ita: Livello di registrazione GUI
+ spa: Nivel de registro GUI
+ zho: GUI日志级别
Gather FFmpeg audio encoders:
- deu: Sammeln von FFmpeg-Audio-Encodern
+ deu: FFmpeg-Audio-Encoder abfragen
eng: Gather FFmpeg audio encoders
fra: Rassembler les encodeurs audio FFmpeg
ita: Raccogliere gli encoder audio FFmpeg
spa: Reúne los codificadores de audio FFmpeg
zho: Gather FFmpeg audio encoders
Gather FFmpeg version:
- deu: Sammeln Sie FFmpeg-Version
+ deu: FFmpeg-Version abfragen
eng: Gather FFmpeg version
fra: Rassembler la version FFmpeg
ita: Raccogliere la versione FFmpeg
spa: Reunir la versión FFmpeg
zho: 获取FFmpeg版本
Gather FFprobe version:
- deu: Sammeln Sie FFprobe-Version
+ deu: FFprobe-Version abfragen
eng: Gather FFprobe version
fra: Rassembler la version FFprobe
ita: Raccogliere la versione FFprobe
@@ -1007,7 +1168,7 @@ Google's VP9 HDR Encoding Guide:
spa: Guía de codificación HDR VP9 de Google
zho: 谷歌VP9 HDR编码指南(英文)
HDR -> SDR Tone Map:
- deu: HDR -> SDR-Tonwertkarte
+ deu: HDR -> SDR Tone Mapping
eng: HDR -> SDR Tone Map
fra: HDR -> SDR Carte des tons
ita: HDR -> Mappa dei toni SDR
@@ -1042,7 +1203,7 @@ HDR10+ Optimizations:
spa: Optimizaciones del HDR10+
zho: HDR10+优化
Have to select a video first:
- deu: Sie müssen zuerst ein Video auswählen
+ deu: Es muss zuerst ein Video ausgewählt werden
eng: Have to select a video first
fra: Il faut d'abord sélectionner une vidéo
ita: Devi prima selezionare un video
@@ -1063,19 +1224,26 @@ Help:
spa: Ayuda
zho: 帮助
Hide NAL unit messages:
- deu: NAL-Geräte-Meldungen ausblenden
- eng: Hide NAL unit messages
- fra: Cacher les messages de l'unité NAL
- ita: Nascondere i messaggi delle unità NAL
- spa: Ocultar los mensajes de la unidad NAL
- zho: 隐藏NAL unit信息
+ deu: NAL-Meldungen (AVC/HEVC Network Abstraction Layer) ausblenden
+ eng: Hide NAL unit (AVC/HEVC Network Abstraction Layer) messages
+ fra: Cacher les messages de l'unité NAL (AVC/HEVC Network Abstraction Layer)
+ ita: Nascondere i messaggi delle unità NAL (AVC/HEVC Network Abstraction Layer)
+ spa: Ocultar los mensajes de la unidad NAL (AVC/HEVC Network Abstraction Layer)
+ zho: 隐藏NAL unit信息 (AVC/HEVC 網路抽象層)
Horizontal Flip:
- deu: Horizontal Flip
+ deu: Horizontal spiegeln
eng: Horizontal Flip
fra: Renvoi horizontal
ita: Capovolgimento orizzontale
spa: Volteo horizontal
zho: 水平翻转
+Init Q:
+ deu: Init Q
+ eng: Init Q
+ fra: Init Q
+ ita: Init Q
+ spa: Init Q
+ zho: 启动Q
Initialize Encoders:
deu: Kodierer initialisieren
eng: Initialize Encoders
@@ -1105,14 +1273,14 @@ Intra-refresh:
spa: Intra-refresco
zho: 帧内刷新
Invalid Crop:
- deu: Ungültiger Zuschnitt
+ deu: Ungültiges Cropping
eng: Invalid Crop
fra: Récolte non valable
ita: Raccolto non valido
spa: Cultivo inválido
zho: 无效裁剪
It is recommended that AQ-mode be enabled along with this feature:
- deu: Es wird empfohlen, dass der AQ-Modus zusammen mit dieser Funktion aktiviert wird
+ deu: Es wird empfohlen, dass zusammen mit dieser Funktion auch der AQ-Mode aktiviert wird
eng: It is recommended that AQ-mode be enabled along with this feature
fra: Il est recommandé d'activer le mode AQ en même temps que cette fonction
ita: Si raccomanda di abilitare la modalità AQ insieme a questa funzione
@@ -1154,7 +1322,7 @@ LICENSES:
spa: LICENCIAS
zho: 许可
Landscape Cover:
- deu: Abdeckung im Querformat
+ deu: Querformatiges Cover
eng: Landscape Cover
fra: Couverture du paysage
ita: Copertura del paesaggio
@@ -1174,20 +1342,34 @@ Left:
ita: Sinistra
spa: Izquierda
zho: 左侧
+Level:
+ deu: Pegel
+ eng: Level
+ fra: Niveau
+ ita: Livello
+ spa: Nivel
+ zho: 级别
Log2 of number of tile columns to encode faster (lesser quality):
- deu: Log2 der Anzahl der Kachelspalten, um schneller zu kodieren (geringere Qualität)
+ deu: Log2 der Anzahl der Gitterspalten, um schneller zu kodieren (geringere Qualität)
eng: Log2 of number of tile columns to encode faster (lesser quality)
fra: Log2 du nombre de colonnes de tuiles pour un encodage plus rapide (qualité moindre)
ita: Log2 del numero di colonne di tegole da codificare più velocemente (qualità inferiore)
spa: Log2 del número de columnas de azulejos para codificar más rápido (menor calidad)
zho: Log2 of number of tile columns to encode faster (lesser quality)
Log2 of number of tile rows to encode faster (lesser quality):
- deu: Log2 der Anzahl der Kachelzeilen, um schneller zu kodieren (geringere Qualität)
+ deu: Log2 der Anzahl der Gitterzeilen, um schneller zu kodieren (geringere Qualität)
eng: Log2 of number of tile rows to encode faster (lesser quality)
fra: Log2 du nombre de rangées de tuiles pour un encodage plus rapide (qualité moindre)
ita: Log2 del numero di file di tegole da codificare più velocemente (qualità inferiore)
spa: Log2 del número de filas de azulejos para codificar más rápido (menor calidad)
zho: Log2 of number of tile rows to encode faster (lesser quality)
+Lookahead:
+ deu: Lookahead
+ eng: Lookahead
+ fra: Lookahead
+ ita: Lookahead
+ spa: Lookahead
+ zho: 瞻前顾后
Lossless:
deu: Verlustfrei
eng: Lossless
@@ -1209,20 +1391,76 @@ Max Muxing Queue Size:
ita: Dimensione massima della coda di Muxing
spa: Tamaño máximo de la cola Muxing
zho: 最大混流队列大小
+Max Q:
+ deu: Max Q
+ eng: Max Q
+ fra: Max Q
+ ita: Q massimo
+ spa: Max Q
+ zho: 最大Q值
Maximum B frames:
- deu: Maximale B-Frames
+ deu: Max. Anzahl B-Frames
eng: Maximum B frames
fra: Cadres B maximum
ita: Telaio massimo B
spa: Máximo de cuadros B
zho: 最大B帧数量
'Maximum number of consecutive b-frames. ':
- deu: 'Maximale Anzahl von aufeinanderfolgenden B-Frames. '
+ deu: 'Maximale Anzahl aufeinanderfolgender B-Frames. '
eng: 'Maximum number of consecutive b-frames. '
fra: "Nombre maximum d'images B consécutives. "
ita: 'Numero massimo di b-frame consecutivi. '
spa: 'Número máximo de fotogramas B consecutivos. '
zho: 连续b帧的最大数量。
+Maxrate:
+ deu: Maxrate
+ eng: Maxrate
+ fra: Maxrate
+ ita: Maxrate
+ spa: Maxrate
+ zho: Maxrate
+Metrics:
+ deu: Metriken
+ eng: Metrics
+ fra: Métriques
+ ita: Metriche
+ spa: Métricas
+ zho: 衡量标准
+Min Q:
+ deu: Min Q
+ eng: Min Q
+ fra: Min Q
+ ita: Min Q
+ spa: Q mínimo
+ zho: 最小Q
+Motion vector accuracy:
+ deu: Genauigkeit des Bewegungsvektors
+ eng: Motion vector accuracy
+ fra: Précision du vecteur de mouvement
+ ita: Precisione del vettore di movimento
+ spa: Precisión del vector de movimiento
+ zho: 运动矢量精度
+Multipass:
+ deu: Multi-pass
+ eng: Multipass
+ fra: Multipass
+ ita: Multipass
+ spa: Multipass
+ zho: 多通道
+NVEncC Encoder support is still experimental!:
+ deu: NVEncC-Encoder-Unterstützung ist immer noch experimentell!
+ eng: NVEncC Encoder support is still experimental!
+ fra: Le support des encodeurs NVEncC est encore expérimental !
+ ita: Il supporto NVEncC Encoder è ancora sperimentale!
+ spa: La compatibilidad con el codificador NVEncC es todavía experimental.
+ zho: NVEncC编码器支持仍然是实验性的!
+NVEncC Options:
+ deu: NVEncC-Optionen
+ eng: NVEncC Options
+ fra: Options NVEncC
+ ita: Opzioni NVEncC
+ spa: Opciones de NVEncC
+ zho: NVEncC选项
New Profile:
deu: Neues Profil
eng: New Profile
@@ -1252,7 +1490,7 @@ No Downmix:
spa: No hay Downmix
zho: 无缩混
No Flip:
- deu: Kein Flip
+ deu: Keine Spiegelung
eng: No Flip
fra: Pas de retournement
ita: Senza capovolgere
@@ -1286,15 +1524,15 @@ No command found for:
ita: Nessun comando trovato per
spa: No se encontró ningún comando para
zho: 没有找到命令
-No crop, scale, rotation,flip nor any other filters will be applied.:
+No crop, scale, rotation, flip nor any other filters will be applied.:
deu: Es werden weder Zuschneiden, Skalieren, Drehen, Spiegeln noch andere Filter angewendet.
- eng: No crop, scale, rotation,flip nor any other filters will be applied.
+ eng: No crop, scale, rotation, flip nor any other filters will be applied.
fra: Aucun filtre ne sera appliqué sur les cultures, les écailles, la rotation, le retournement ou autre.
ita: Non verranno applicati filtri per il raccolto, la scala, la rotazione, il capovolgimento o altri filtri.
spa: No se aplicará ningún filtro de cultivo, de escala, de rotación, de volteo ni ningún otro.
zho: 不会应用裁剪、缩放、旋转、翻转或任何其他过滤器。
No encoding is currently in process, starting encode:
- deu: Es wird gerade keine Kodierung durchgeführt, Kodierung wird gestartet
+ deu: Es wird momentan keine Kodierung durchgeführt, Kodierung wird gestartet
eng: No encoding is currently in process, starting encode
fra: Aucun codage n'est actuellement en cours, le codage de départ
ita: Nessuna codifica è attualmente in corso, iniziando a codificare
@@ -1336,7 +1574,7 @@ Not a video file:
spa: No es un archivo de video
zho: 不是视频文件
Only put the HDR10+ dynamic metadata in the IDR and frames where the values have changed.:
- deu: Setzen Sie die dynamischen HDR10+-Metadaten nur in die IDR und Frames, bei denen sich die Werte geändert haben.
+ deu: Setzen der dynamischen HDR10+-Metadaten nur in die IDR und Frames, bei denen sich die Werte geändert haben.
eng: Only put the HDR10+ dynamic metadata in the IDR and frames where the values have changed.
fra: Ne placez les métadonnées dynamiques HDR10+ que dans l'IDR et les cadres dont les valeurs ont changé.
ita: Mettete i metadati dinamici HDR10+ solo nell'IDR e nei frame in cui i valori sono cambiati.
@@ -1364,14 +1602,14 @@ Open Directory:
spa: Directorio Abierto
zho: 打开目录
Open Log Directory:
- deu: Log-Verzeichnis öffnen
+ deu: Protokoll-Verzeichnis öffnen
eng: Open Log Directory
fra: Répertoire des journaux ouverts
ita: Aprire l'elenco dei log
spa: Directorio abierto de registros
zho: 打开日志目录
Output:
- deu: Ausgang
+ deu: Ausgabe
eng: Output
fra: Sortie
ita: Uscita
@@ -1385,35 +1623,42 @@ Output FPS:
spa: Salida FPS
zho: 输出FPS
Over-allocation of frame threads will not improve performance,:
- deu: Eine Überbelegung von Frame-Threads wird die Leistung nicht verbessern,
+ deu: Die Auswahl von mehr als der verfügbaren Frame-Threads verbessert nicht die Leistung,
eng: Over-allocation of frame threads will not improve performance,
fra: Une allocation excessive des fils de trame n'améliorera pas les performances,
ita: La sovra-assegnazione delle filettature del telaio non migliorerà le prestazioni,
spa: La sobreasignación de hilos del marco no mejorará el rendimiento,
zho: 过多分配帧线程无法提高性能,
Overlay this subtitle track onto the video during conversion.:
- deu: Überlagern Sie diese Untertitelspur während der Konvertierung mit dem Video.
+ deu: Diese Untertitelspur bei der Konvertierung auf das Video überlagern.
eng: Overlay this subtitle track onto the video during conversion.
fra: Superposez cette piste de sous-titres sur la vidéo pendant la conversion.
ita: Sovrapponete questa traccia di sottotitoli al video durante la conversione.
spa: Superponga esta pista de subtítulos en el vídeo durante la conversión.
zho: 在转换时将此字幕轨叠加到视频上。
Override Source FPS:
- deu: Überschreiben Quell-FPS
+ deu: Quell-FPS überschreiben
eng: Override Source FPS
fra: Annuler la source FPS
ita: Annullare la sorgente FPS
spa: Anular el FPS de la fuente
zho: 覆盖来源 FPS
+Override the preset rate-control:
+ deu: voreingestelle Rate Control überschreiben
+ eng: Override the preset rate-control
+ fra: Annuler le contrôle des taux préétablis
+ ita: Sovrascrivere il controllo del tasso preimpostato
+ spa: Anula el control de velocidad preestablecido
+ zho: 覆盖预设的速率控制
PIR can replace keyframes by inserting a column of intra blocks in non-keyframes,:
- deu: PIR kann Keyframes durch Einfügen einer Spalte von Intrablöcken in Nicht-Keyframes ersetzen,
+ deu: PIR kann durch Einfügen einer Spalte von Intrablöcken in Nicht-Keyframes Keyframes ersetzen,
eng: PIR can replace keyframes by inserting a column of intra blocks in non-keyframes,
fra: Le PIR peut remplacer les images clés en insérant une colonne de blocs intra dans les images non clés,
ita: Il PIR può sostituire i keyframe inserendo una colonna di blocchi interni nei non-keyframe,
spa: PIR puede reemplazar los fotogramas clave insertando una columna de intrabloques en los fotogramas no clave,
zho: PIR可以在非关键帧中插入一列intra blocks,从而替代关键帧。
Parse Video details:
- deu: Video-Details parsen
+ deu: Video-Details analysieren
eng: Parse Video details
fra: Détails de la vidéo de Parse
ita: Dettagli Parse Video
@@ -1427,7 +1672,7 @@ Pause / Resume the current command:
spa: Pausa / Reanudar el comando actual
zho: 暂停/恢复当前命令
Pause Encode:
- deu: Pause Encode
+ deu: Kodierung pausieren
eng: Pause Encode
fra: Pause Encode
ita: Pausa Codificare
@@ -1447,29 +1692,36 @@ Pixel Format (requires at least 10-bit for HDR):
ita: Formato pixel (richiede almeno 10 bit per HDR)
spa: Formato de píxeles (requiere al menos 10 bits para el HDR)
zho: 像素格式(HDR要求至少10-bit)
+Please make sure seek method is set to exact:
+ deu: Bitte sicherstellen, dass die Suchmethode auf exakt eingestellt ist
+ eng: Please make sure seek method is set to exact
+ fra: Veuillez vous assurer que la méthode de recherche est réglée sur l'exacte
+ ita: Si prega di assicurarsi che il metodo di ricerca sia impostato su
+ spa: Por favor, asegúrese de que el método de búsqueda se establece con exactitud
+ zho: 请确保检索方式已设置为exact
Please provide a profile name:
- deu: Bitte geben Sie einen Profilnamen an
+ deu: Bitte einen Profilnamen angeben
eng: Please provide a profile name
fra: Veuillez fournir un nom de profil
ita: Si prega di fornire un nome di profilo
spa: Por favor, proporcione un nombre de perfil
zho: 请提供方案名称
Please report this issue:
- deu: Bitte melden Sie dieses Problem
+ deu: Bitte dieses Problem melden
eng: Please report this issue
fra: Veuillez signaler ce problème
ita: Si prega di segnalare questo problema
spa: Por favor, informe de este asunto
zho: 请报告这个问题
Please restart FastFlix to apply settings:
- deu: Bitte starten Sie FastFlix neu, um die Einstellungen zu übernehmen
+ deu: Bitte FastFlix neustarten, um die Einstellungen zu übernehmen
eng: Please restart FastFlix to apply settings
fra: Por favor, reinicie FastFlix para aplicar la configuración
ita: Riavviare FastFlix per applicare le impostazioni
spa: Por favor, reinicie FastFlix para aplicar la configuración
zho: 请重新启动FastFlix以应用设置
Poster Cover:
- deu: Poster-Cover
+ deu: Plakat-Cover
eng: Poster Cover
fra: Couverture de l'affiche
ita: Copertina per poster
@@ -1489,6 +1741,13 @@ Preset:
ita: Preset
spa: Preset
zho: 预设
+Profile Name:
+ deu: Profil-Name
+ eng: Profile Name
+ fra: Nom du profil
+ ita: Nome del profilo
+ spa: Nombre del perfil
+ zho: 方案名称
Profile_encoderopt:
deu: Profil
eng: Profile
@@ -1510,13 +1769,6 @@ Profile_window:
ita: Profilo
spa: Perfil
zho: 方案
-Profile Name:
- deu: Profil-Name
- eng: Profile Name
- fra: Nom du profil
- ita: Nome del profilo
- spa: Nombre del perfil
- zho: 方案名称
Profiles:
deu: Profile
eng: Profiles
@@ -1531,6 +1783,13 @@ Python:
ita: Python
spa: Python
zho: Python
+Q-pel is highest precision:
+ deu: Q-pel ist die höchste Präzision
+ eng: Q-pel is highest precision
+ fra: Le Q-pel est de la plus haute précision
+ ita: Q-pel è la massima precisione
+ spa: Q-pel es la máxima precisión
+ zho: Q-pel是最高精度
Quality:
deu: Qualität
eng: Quality
@@ -1539,28 +1798,28 @@ Quality:
spa: Calidad
zho: 质量
Quality and compression efficiency vs speed trade-off:
- deu: Kompromiss zwischen Qualität und Kompressionseffizienz und Geschwindigkeit
+ deu: Kompromiss zwischen Qualität/Komprimierungs-Effizienz und Geschwindigkeit
eng: Quality and compression efficiency vs speed trade-off
fra: Le compromis entre la qualité et l'efficacité de la compression et la vitesse
ita: Qualità ed efficienza di compressione rispetto al compromesso velocità
spa: La calidad y la eficiencia de la compresión frente a la compensación de la velocidad
zho: 在质量及压缩效率与速度之间进行权衡
Quality/Speed ratio modifier:
- deu: Qualität/Geschwindigkeitsverhältnis-Modifikator
+ deu: Modifikator Qualitäts/Geschwindigkeits-Verhältnis
eng: Quality/Speed ratio modifier
fra: Modificateur du rapport qualité/vitesse
ita: Modificatore rapporto qualità/velocità
spa: Modificador de la relación calidad/velocidad
zho: 调整质量与速度之比
Quality/Speed ratio modifier (defaults to -1):
- deu: 'Qualitäts-/Geschwindigkeitsverhältnis-Modifikator (Standardwert: -1)'
+ deu: 'Modifikator Qualitäts/Geschwindigkeits-Verhältnis (Standardwert: -1)'
eng: Quality/Speed ratio modifier (defaults to -1)
fra: Modificateur du rapport qualité/vitesse (par défaut à -1)
ita: Modificatore del rapporto qualità/velocità (valori predefiniti a -1)
spa: Modificador de la relación calidad/velocidad (por defecto a -1)
zho: 调整质量与速度之比(默认值为-1)
Quality/Speed ratio modifier (defaults to 4):
- deu: 'Modifikator für das Verhältnis Qualität/Geschwindigkeit (Standardwert: 4)'
+ deu: 'Modifikator Qualitäts/Geschwindigkeits-Verhältnis (Standardwert: 4)'
eng: Quality/Speed ratio modifier (defaults to 4)
fra: Modificateur du rapport qualité/vitesse (par défaut à 4)
ita: Modificatore del rapporto qualità/velocità (valori predefiniti a 4)
@@ -1580,15 +1839,29 @@ Queue has been paused:
ita: La coda è stata messa in pausa
spa: La cola se ha detenido
zho: 队列已暂停
+RC Lookahead:
+ deu: RC Lookahead
+ eng: RC Lookahead
+ fra: RC Lookahead
+ ita: RC Lookahead
+ spa: RC Lookahead
+ zho: RC Lookahead
Raise or lower per-block quantization based on complexity analysis of the source image.:
- deu: Erhöhen oder verringern Sie die Quantisierung pro Block basierend auf der Komplexitätsanalyse des Quellbilds.
+ deu: Erhöhen oder verringern der Quantisierung pro Block basierend auf der Komplexitätsanalyse des Quellbildes.
eng: Raise or lower per-block quantization based on complexity analysis of the source image.
fra: Augmenter ou diminuer la quantification par bloc en fonction de l'analyse de la complexité de l'image source.
ita: Aumentare o diminuire la quantizzazione per blocco in base all'analisi della complessità dell'immagine sorgente.
spa: Aumentar o disminuir la cuantificación por bloque basada en el análisis de la complejidad de la imagen de origen.
zho: 根据对源图像的复杂度分析,提升或降低各个块的量化。
+Rate Control:
+ deu: Ratensteuerung
+ eng: Rate Control
+ fra: Contrôle des tarifs
+ ita: Controllo della velocità
+ spa: Control de velocidad
+ zho: 速率控制
Raw Commands:
- deu: Raw-Befehle
+ deu: Kommandozeilenbefehle
eng: Raw Commands
fra: Commandes brutes
ita: Comandi grezzi
@@ -1602,12 +1875,19 @@ Ready to encode:
spa: Listo para codificar
zho: 准备编码
Reconstructed output pictures are bit-exact to the input pictures.:
- deu: Rekonstruierte Ausgangsbilder sind bit-genau zu den Eingangsbildern.
+ deu: Rekonstruierte Ausgabebilder entsprechen bit-genau den Eingangsbildern.
eng: Reconstructed output pictures are bit-exact to the input pictures.
fra: Les images de sortie reconstruites sont exactes au niveau des bits par rapport aux images d'entrée.
ita: Le immagini di uscita ricostruite sono bit-esatte alle immagini di ingresso.
spa: Las imágenes de salida reconstruidas son un poco exactas a las imágenes de entrada.
zho: 重建后的输出图像与输入图像是逐位一致(bit-exact)的。
+Ref Frames:
+ deu: Ref-Frames
+ eng: Ref Frames
+ fra: Ref Frames
+ ita: Fotogrammi di rif.
+ spa: Fotogramas de referencia
+ zho: 参考框架
Remove HDR:
deu: HDR entfernen
eng: Remove HDR
@@ -1630,21 +1910,21 @@ Remove completed tasks:
spa: Eliminar las tareas completadas
zho: 删除已完成的任务
Removing after done command:
- deu: Entfernen nach erfolgtem Befehl
+ deu: Entfernen nach erledigtem Befehl
eng: Removing after done command
fra: Suppression après commande
ita: Rimozione dopo aver eseguito il comando
spa: Retirar después de la orden hecha
zho: 在完成命令后删除
Repeat Headers:
- deu: Kopfzeilen wiederholen
+ deu: Header wiederholen
eng: Repeat Headers
fra: Répéter les en-têtes
ita: Ripetere le intestazioni
spa: Repetición de los encabezados
zho: 重复标头
Report Issue:
- deu: Ausgabe melden
+ deu: Problem melden
eng: Report Issue
fra: Numéro du rapport
ita: Segnala il problema
@@ -1700,7 +1980,7 @@ Row multithreading:
spa: 'Corriendo tras el mando hecho:'
zho: 在完成命令后运行。
Running command:
- deu: Laufendes Kommando
+ deu: Laufender Befehl
eng: Running command
fra: Commande en cours d'exécution
ita: Comando in esecuzione
@@ -1714,7 +1994,7 @@ SVT-AV1 Encoding Guide:
spa: Guía de codificación del SVT-AV1
zho: SVT-AV1编码指南
Same as Source:
- deu: Gleich wie die Quelle
+ deu: identisch zur Quelle
eng: Same as Source
fra: Même que la source
ita: Uguale alla fonte
@@ -1734,6 +2014,13 @@ Save Commands:
ita: Comandi di salvataggio
spa: Salvar los comandos
zho: 保存命令
+Save File:
+ deu: Datei speichern
+ eng: Save File
+ fra: Enregistrer le fichier
+ ita: Salva file
+ spa: Guardar archivo
+ zho: 保存文件
Save commands to file:
deu: Befehle in Datei speichern
eng: Save commands to file
@@ -1749,28 +2036,56 @@ Scale:
spa: Escala
zho: 缩放
Scrub away all incoming metadata, like video titles, unique markings and so on.:
- deu: Entfernen Sie alle eingehenden Metadaten, wie Videotitel, eindeutige Markierungen und so weiter.
+ deu: Entfernen aller eingehenden Metadaten, wie Videotitel, eindeutige Markierungen usw.
eng: Scrub away all incoming metadata, like video titles, unique markings and so on.
fra: Supprimez toutes les métadonnées entrantes, comme les titres des vidéos, les marquages uniques, etc.
ita: Cancella tutti i metadati in arrivo, come i titoli dei video, le marcature uniche e così via.
spa: Borra todos los metadatos entrantes, como títulos de video, marcas únicas y demás.
zho: 擦除输入文件中所有的元数据,如视频标题、唯一标记等。
+Selects which NVENC capable GPU to use. First GPU is 0, second is 1, and so on:
+ deu: Wählt aus, welche NVENC-fähige GPU genutzt werden soll. Die erste GPU ist 0, die zweite 1, usw
+ eng: Selects which NVENC capable GPU to use. First GPU is 0, second is 1, and so on
+ fra: Sélectionne le GPU compatible NVENC à utiliser. Le premier GPU est 0, le second est 1, et ainsi de suite
+ ita: Seleziona quale GPU con capacità NVENC utilizzare. La prima GPU è 0, la seconda è 1, e così via
+ spa: Selecciona qué GPU con capacidad NVENC se va a utilizar. La primera GPU es 0, la segunda es 1, y así sucesivamente
+ zho: 选择使用哪个NVENC功能的GPU。第一个GPU为0,第二个为1,以此类推。
+Set speed to 4 for first pass:
+ deu: Setze Geschwindigkeit des ersten Durchlaufs auf 4
+ eng: Set speed to 4 for first pass
+ fra: Régler la vitesse à 4 pour le premier passage
+ ita: Imposta la velocità a 4 per il primo passaggio
+ spa: Establece la velocidad en 4 para la primera pasada
+ zho: 第一遍速度设置为4
Set the "title" tag, sometimes shown as "Movie Name":
- deu: Setzen Sie den "Titel"-Tag, der manchmal als "Filmname" angezeigt wird
+ deu: Setzen des "Titel"-Tags, der manchmal als "Filmname" angezeigt wird
eng: Set the "title" tag, sometimes shown as "Movie Name"
fra: Poner la etiqueta "título", a veces se muestra como "Nombre de la película"
ita: Impostare il tag "title", a volte mostrato come "Movie Name" (nome del film)
spa: Poner la etiqueta "título", a veces se muestra como "Nombre de la película"
zho: 设置“标题”(title,有时显示为“电影名称”(Movie Name))标签
+Set the encoding level restriction:
+ deu: Kodierungslevel-Einschränkung festlegen
+ eng: Set the encoding level restriction
+ fra: Définir la restriction du niveau d'encodage
+ ita: Imposta la restrizione del livello di codifica
+ spa: Establezca la restricción del nivel de codificación
+ zho: 设置编码级别限制
+Set the encoding tier:
+ deu: Kodierungsstufe festlegen
+ eng: Set the encoding tier
+ fra: Définir le niveau d'encodage
+ ita: Imposta il livello di codifica
+ spa: Establecer el nivel de codificación
+ zho: 设置编码层
Set the level of effort in determining B frame placement.:
- deu: Legen Sie den Grad des Aufwands bei der Bestimmung der B-Frame-Platzierung fest.
+ deu: Grad des Aufwands bei der Bestimmung der B-Frame-Platzierung festlegen.
eng: Set the level of effort in determining B frame placement.
fra: Définir le niveau d'effort pour déterminer le placement des images B.
ita: Impostare il livello di sforzo nel determinare il posizionamento del fotogramma B.
spa: Establezca el nivel de esfuerzo para determinar la ubicación del cuadro B.
zho: 对决定B帧位置时的工作量水平进行调整。
'Setting after done command to:':
- deu: 'Einstellung nach dem Befehl done auf:'
+ deu: 'Einstellung nach erledigtem Befehl setzen auf:'
eng: 'Setting after done command to:'
fra: 'Régler après avoir fait la commande à :'
ita: 'Impostare dopo il comando su:'
@@ -1784,28 +2099,28 @@ Settings:
spa: Ajustes
zho: 设置
Single Pass (Bitrate):
- deu: Einzelner Durchlauf (Bitrate)
+ deu: Ein einzelner Durchlauf (Bitrate)
eng: Single Pass (Bitrate)
fra: Passage unique (Bitrate)
ita: Passaggio singolo (Bitrato)
spa: Pase único (Bitrate)
zho: 一遍编码(比特率)
Single Pass (CRF):
- deu: Einzelner Durchlauf (CRF)
+ deu: Ein einzelner Durchlauf (CRF)
eng: Single Pass (CRF)
fra: Laissez-passer unique (CRF)
ita: Passaggio singolo (CRF)
spa: Pase único (CRF)
zho: 一遍编码(CRF)
Size Estimate:
- deu: Größe Schätzung
+ deu: geschätzte Größe
eng: Size Estimate
fra: Estimation de la taille
ita: Stima delle dimensioni
spa: Estimación del tamaño
zho: 预计文件大小
Slow is highest personal recommenced, as past that is much smaller gains:
- deu: Langsam ist die höchste persönliche Empfehlung, da danach viel kleinere Gewinne
+ deu: Slow ist die maximale persönliche Empfehlung, da die Zugewinne bei noch langsamerem deutlich kleiner sind.
eng: Slow is highest personal recommenced, as past that is much smaller gains
fra: La lenteur est la plus haute personnelle recommencée, comme passé c'est des gains beaucoup plus petits
ita: Lento è più alto personale è ricominciato, come passato che è molto più piccolo guadagni
@@ -1833,7 +2148,7 @@ Source Details:
spa: Detalles de la fuente
zho: 来源详情
Source Frame Rate:
- deu: Quell-Bildrate
+ deu: Bildrate der Quelle
eng: Source Frame Rate
fra: Taux de trame source
ita: Frame Rate della fonte
@@ -1853,6 +2168,13 @@ Source width:
ita: Larghezza della fonte
spa: Ancho de la fuente
zho: 源文件宽度
+Spatial AQ:
+ deu: Spatila AQ
+ eng: Spatial AQ
+ fra: QA spatiale
+ ita: AQ spaziale
+ spa: AQ espacial
+ zho: 空间空气质量
Speed:
deu: Geschwindigkeit
eng: Speed
@@ -1861,7 +2183,7 @@ Speed:
spa: Velocidad
zho: 速度
Start:
- deu: Beginn
+ deu: Start
eng: Start
fra: Démarrer
ita: Iniziare
@@ -1889,7 +2211,7 @@ Subtitle Tracks:
spa: Pistas de subtítulos
zho: 字幕轨
Subtitle select language:
- deu: Untertitel Sprache wählen
+ deu: Untertitel-Sprache wählen
eng: Subtitle select language
fra: Sous-titre choisir la langue
ita: Sottotitolo selezionare la lingua
@@ -1909,6 +2231,13 @@ Success:
ita: Successo
spa: Éxito
zho: 成功
+Support FastFlix:
+ deu: Unterstützt FastFlix
+ eng: Support FastFlix
+ fra: Soutenez FastFlix
+ ita: Supporto FastFlix
+ spa: Soporta FastFlix
+ zho: 支持FastFlix
Supported Image Files:
deu: Unterstützte Bilddateien
eng: Supported Image Files
@@ -1917,7 +2246,7 @@ Supported Image Files:
spa: Archivos de imagen soportados
zho: 支持的图像文件
The GUI might have died, but I'm going to keep converting!:
- deu: Die GUI mag gestorben sein, aber ich werde weiter konvertieren!
+ deu: Die GUI ist eventuell abgestürzt, aber ich werde weiter konvertieren!
eng: The GUI might have died, but I'm going to keep converting!
fra: L'interface graphique est peut-être morte, mais je vais continuer à me convertir !
ita: L'interfaccia grafica sarà anche morta, ma continuerò a convertirmi!
@@ -1931,14 +2260,14 @@ The more complex the block, the more quantization is used.:
spa: Cuanto más complejo es el bloque, más cuantificación se utiliza.
zho: 越复杂的块,使用的量化也越高。
The purpose is to prevent blocking or banding artifacts in regions with few/zero AC coefficients.:
- deu: Der Zweck ist, Blocking- oder Banding-Artefakte in Regionen mit wenigen/null AC-Koeffizienten zu verhindern.
+ deu: Der Zweck ist, Blocking- oder Banding-Artefakte in Regionen mit wenigen/keinen AC-Koeffizienten zu verhindern.
eng: The purpose is to prevent blocking or banding artifacts in regions with few/zero AC coefficients.
fra: L'objectif est d'éviter de bloquer ou de banderoler des artefacts dans des régions où les coefficients AC sont faibles ou nuls.
ita: Lo scopo è quello di prevenire il blocco o il banding di artefatti in regioni con pochi/zeri coefficienti AC.
spa: El propósito es prevenir el bloqueo o los artefactos de bandas en regiones con coeficientes de CA bajos/cero.
zho: 目的是为了防止在AC coefficients较少或为零的区域出现blocking或banding artifacts。
There is a conversion in process!:
- deu: Es ist eine Konvertierung im Gange!
+ deu: Es ist eine Konvertierung am laufen!
eng: There is a conversion in process!
fra: Il y a une conversion en cours !
ita: C'è una conversione in corso!
@@ -1966,14 +2295,14 @@ This flag performs bi-linear interpolation of the corner reference samples for a
spa: Este banderín realiza una interpolación bi-línea de las muestras de referencia de las esquinas para un fuerte efecto de suavizado.
zho: 这个选项对corner reference samples进行双线性插值,以获得强平滑效果。
This improves encoding speed significantly on systems that are otherwise underutilised when encoding VP9.:
- deu: Dies verbessert die Kodiergeschwindigkeit auf Systemen, die sonst bei der Kodierung von VP9 nicht ausgelastet sind, erheblich.
+ deu: Dies verbessert erheblich die Kodiergeschwindigkeit auf Systemen, die anonsten bei der Kodierung von VP9 unausgelastet sind.
eng: This improves encoding speed significantly on systems that are otherwise underutilised when encoding VP9.
fra: Cela améliore considérablement la vitesse de codage sur des systèmes qui sont autrement sous-utilisés lors du codage VP9.
ita: Questo migliora la velocità di codifica in modo significativo su sistemi che sono altrimenti sottoutilizzati durante la codifica VP9.
spa: Esto mejora significativamente la velocidad de codificación en los sistemas que de otra manera son subutilizados al codificar el VP9.
zho: 在编码VP9时资源利用不足的系统上,此选项能够显著提升编码速度。
This is intended for use when you do not have a container to keep the stream headers for you:
- deu: Dies ist für die Verwendung vorgesehen, wenn Sie keinen Container haben, der die Stream-Header für Sie aufbewahrt
+ deu: Dies ist für die Verwendung vorgesehen, wenn kein Container vorhanden ist, der die Stream-Header aufbewahrt
eng: This is intended for use when you do not have a container to keep the stream headers for you
fra: Il est destiné à être utilisé lorsque vous ne disposez pas d'un conteneur pour conserver les en-têtes de flux
ita: Questo è destinato all'uso quando non si dispone di un contenitore per mantenere le intestazioni dello stream per voi
@@ -1994,7 +2323,7 @@ This is used for ultra-high bitrates with zero loss of quality.:
spa: 'Esta opción no se reinicia a menos que se necesite conformar '
zho: 除非您需要为了刻录到物理光盘而遵守蓝光标准,
This will just copy the video track as is.:
- deu: Damit wird der Videotrack einfach so kopiert, wie er ist.
+ deu: Hiermit wird die Videospur einfach so kopiert, wie sie ist.
eng: This will just copy the video track as is.
fra: Il suffit de copier la piste vidéo telle quelle.
ita: Questo si limiterà a copiare la traccia video così com'è.
@@ -2029,14 +2358,14 @@ Tiles:
spa: Baldosas
zho: Tiles
Time Left:
- deu: Linke Zeit
+ deu: Verbleibende Dauer
eng: Time Left
fra: Temps restant
ita: Tempo rimanente
spa: Tiempo restante
zho: 剩余时间
Time Elapsed:
- deu: Verstrichene Zeit
+ deu: Verstrichene Dauer
eng: Time Elapsed
fra: Temps écoulé
ita: Tempo trascorso
@@ -2064,14 +2393,14 @@ Total video height must be greater than 0:
spa: La altura total del video debe ser mayor que 0
zho: 视频总高度必须大于0
Tune:
- deu: Abstimmen
+ deu: Feineinstellung
eng: Tune
fra: Tune
ita: Tune
spa: Sintoniza
zho: 调校
Tune the settings for a particular type of source or situation:
- deu: Abstimmen der Einstellungen für einen bestimmten Quellentyp oder eine bestimmte Situation
+ deu: Feineinstellungen für einen bestimmten Quelltyp oder eine bestimmte Situation
eng: Tune the settings for a particular type of source or situation
fra: Régler les paramètres pour un type de source ou une situation particulière
ita: Sintonizzare le impostazioni per un particolare tipo di sorgente o situazione
@@ -2092,14 +2421,21 @@ Usage:
spa: Uso
zho: 用法
Use --bframes 0 to force all P/I low-latency encodes.:
- deu: Verwenden Sie --bframes 0, um alle P/I-Codierungen mit niedriger Latenz zu erzwingen.
+ deu: --bframes 0 verwenden, um alle P/I-Codierungen mit niedriger Latenz zu erzwingen.
eng: Use --bframes 0 to force all P/I low-latency encodes.
fra: Utilisez --bframes 0 pour forcer tous les codes P/I à faible latence.
ita: Utilizzare --bframes 0 per forzare tutte le codifiche P/I a bassa latenza.
spa: Use --bframes 0 para forzar todos los códigos de baja latencia P/I.
zho: 使用--bframes 0强制进行全P/I帧的低延迟编码。
-Use Sane Audio Selection (updatable in config file):
- deu: Sane Audio Selection verwenden (aktualisierbar in der Konfigurationsdatei)
+Use B frames as references:
+ deu: B-Frames als Referenz nutzen
+ eng: Use B frames as references
+ fra: Utiliser les cadres B comme références
+ ita: Usa i fotogrammi B come riferimenti
+ spa: Utilizar los fotogramas B como referencia
+ zho: 用B帧作为参考
+Use Sane Audio Selection (customizable in config file):
+ deu: Nur sinnvolle Audioformate vorschlagen (anpassbar in der Konfigurationsdatei)
eng: Use Sane Audio Selection (updatable in config file)
fra: Utiliser la sélection audio Sane (actualisable dans le fichier de configuration)
ita: Utilizzare Sane Audio Selection (aggiornabile nel file di configurazione)
@@ -2113,7 +2449,7 @@ Useful when there is a desire to signal 0 values for max-cll and max-fall.:
spa: Es útil cuando se desea señalar los valores 0 para max-cll y max-fall.
zho: 当需要将max-cll及max-fall置0值时有用。
Useful when you have the "Too many packets buffered for output stream" error:
- deu: Nützlich, wenn Sie den Fehler "Too many packets buffered for output stream" haben
+ deu: Nützlich, wenn der Fehler "Too many packets buffered for output stream" auftritt
eng: Useful when you have the "Too many packets buffered for output stream" error
fra: Utile lorsque vous avez l'erreur "Too many packets buffered for output stream
ita: Utile quando si ha l'errore "Troppi pacchetti bufferizzati per il flusso di uscita".
@@ -2127,12 +2463,19 @@ Using 1 or 2 will increase encoding speed at the expense of having some impact o
spa: El uso de 1 o 2 aumentará la velocidad de codificación a expensas de tener algún impacto en la calidad y la precisión del control de la tasa.
zho: 使用1或2会提高编码速度,但代价是对质量和码率控制精度有一定影响。
Using a single frame thread gives a slight improvement in compression,:
- deu: Die Verwendung eines einzelnen Frame-Threads führt zu einer leichten Verbesserung der Komprimierung,
+ deu: Die Verwendung eines einzigen Frame-Threads führt zu einer leichten Verbesserung der Komprimierung,
eng: Using a single frame thread gives a slight improvement in compression,
fra: L'utilisation d'un seul fil de trame donne une légère amélioration de la compression,
ita: L'utilizzo di una filettatura a telaio singolo offre un leggero miglioramento della compressione,
spa: Usar un solo hilo de cuadro da una ligera mejora en la compresión,
zho: 使用单帧线程会使压缩率略有提高,
+VBR Target:
+ deu: VBR Zielrate
+ eng: VBR Target
+ fra: Cible VBR
+ ita: Obiettivo VBR
+ spa: Objetivo VBR
+ zho: VBR目标
'Values: 0:none; 1:fast; 2:full(trellis) default':
deu: 'Werte: 0:keine; 1:schnell; 2:voll(trellis) Standard'
eng: 'Values: 0:none; 1:fast; 2:full(trellis) default'
@@ -2155,14 +2498,14 @@ Various:
spa: Varios
zho: 多种许可
Vert + Hoz Flip:
- deu: Vert + Hoz Flip
+ deu: Vert + Hoz spiegeln
eng: Vert + Hoz Flip
fra: Vert + Hoz Flip
ita: Vert + Hoz Flip
spa: Vert + Hoz Flip
zho: 垂直+水平翻转
Vertical Flip:
- deu: Vertikaler Flip
+ deu: Vertikal spiegeln
eng: Vertical Flip
fra: Volteo vertical
ita: Capovolgimento verticale
@@ -2246,14 +2589,14 @@ Width must be divisible by 2:
spa: El ancho debe ser divisible por 2
zho: 宽度必须能被2整除
Width must be divisible by 2 - Source width:
- deu: Breite muss durch 2 teilbar sein - Quellbreite
+ deu: Breite muss durch 2 teilbar sein - Breite der Quelle
eng: Width must be divisible by 2 - Source width
fra: La largeur doit être divisible par 2 - Largeur de la source
ita: La larghezza deve essere divisibile per 2 - Larghezza della sorgente
spa: El ancho debe ser divisible por 2 - Ancho de la fuente
zho: 宽度必须能被2整除--源文件宽度
Will fix first subtitle track to not be default:
- deu: Repariert die erste Untertitelspur so, dass sie nicht standardmäßig ist
+ deu: Hiermit wird die erste Untertitelspur nicht mehr als Standard gesetzt
eng: Will fix first subtitle track to not be default
fra: Fixera la première piste de sous-titres pour qu'elle ne soit pas par défaut
ita: Correggerà la prima traccia dei sottotitoli per non essere predefinita
@@ -2267,7 +2610,7 @@ With b-adapt 0, the GOP structure is fixed based on the values of --keyint and -
spa: Con b-adaptado 0, la estructura del GOP se fija en base a los valores de --keyint y --bframes.
zho: 当b-adapt为0时,图像组(Group Of Pictures, GOP)结构是根据--keyint和--bframes的值确定并固定的。
With b-adapt 1 a light lookahead is used to choose B frame placement.:
- deu: Mit b-adapt 1 wird ein leichter Lookahead verwendet, um die B-Frame-Platzierung zu wählen.
+ deu: Mit b-adapt 1 wird ein wenig vorausgeschaut, um die B-Frame-Platzierung zu wählen.
eng: With b-adapt 1 a light lookahead is used to choose B frame placement.
fra: Avec l'adaptateur b 1, un léger regard est utilisé pour choisir le placement des images B.
ita: Con b-adapt 1 si usa un leggero lookahead per scegliere il posizionamento del telaio B.
@@ -2295,7 +2638,7 @@ Yes:
spa: Sì
zho: 是
You are using the latest version of FastFlix:
- deu: Sie verwenden die neueste Version von FastFlix
+ deu: Die aktuellste Version von FastFlix wird verwendet
eng: You are using the latest version of FastFlix
fra: Vous utilisez la dernière version de FastFlix
ita: State utilizzando l'ultima versione di FastFlix
@@ -2316,14 +2659,14 @@ already exists:
spa: ya existe
zho: 已有
and the amount of work performed by the full trellis version of --b-adapt lookahead.:
- deu: und der Arbeitsaufwand, der von der Full Trellis-Version von --b-adapt lookahead durchgeführt wird.
+ deu: und der Arbeitsaufwand, der bei --b-adapt 2 (full trellis) durchgeführt wird.
eng: and the amount of work performed by the full trellis version of --b-adapt lookahead.
fra: et la quantité de travail effectuée par la version complète en treillis de --b-adapt lookahead.
ita: e la quantità di lavoro svolto dalla versione completa della versione a traliccio di --b-adattate lookahead.
spa: y la cantidad de trabajo realizado por la versión completa de la espaldera de --b-adaptado lookahead.
zho: lookahead在full(trellis)模式下执行的工作量有二次方的影响。
and you want keyframes to be random access points.:
- deu: und Sie wollen, dass Keyframes zufällige Zugriffspunkte sind.
+ deu: und es ist gewünscht, dass die Keyframes zufällige Zugriffspunkte sind.
eng: and you want keyframes to be random access points.
fra: et vous voulez que les images clés soient des points d'accès aléatoires.
ita: e si desidera che i fotogrammi chiave siano punti di accesso casuali.
@@ -2344,7 +2687,7 @@ are mere suggestions!:
spa: son meras sugerencias!
zho: 的对应关系仅供参考
attachment tracks found:
- deu: Anhangspuren gefunden
+ deu: Dateianhang-Spuren gefunden
eng: attachment tracks found
fra: pistes d'attache trouvées
ita: tracce di attacco trovate
@@ -2365,7 +2708,7 @@ b-adapt:
spa: b-adapt
zho: b-adapt
'b-adapt: Set the level of effort in determining B frame placement.':
- deu: 'b-adapt: Legen Sie den Grad des Aufwands bei der Bestimmung der B-Frame-Platzierung fest.'
+ deu: 'b-adapt: Festlegen des Grades des Aufwands bei der Bestimmung der B-Frame-Platzierung.'
eng: 'b-adapt: Set the level of effort in determining B frame placement.'
fra: "b-adapt : Fixe le niveau d'effort pour déterminer le placement de l'image B."
ita: 'b-adatta: Impostare il livello di sforzo nel determinare il posizionamento del telaio B.'
@@ -2379,7 +2722,7 @@ bad micro value:
spa: mal valor micro
zho: bad micro value
best is recommended if you have lots of time and want the best compression efficiency.:
- deu: best wird empfohlen, wenn Sie viel Zeit haben und die beste Komprimierungseffizienz wünschen.
+ deu: best wird empfohlen, wenn viel Zeit zur Verfügung steht und die beste Komprimierungseffizienz gewünscht ist.
eng: best is recommended if you have lots of time and want the best compression efficiency.
fra: Le meilleur est recommandé si vous avez beaucoup de temps et si vous voulez obtenir la meilleure efficacité de compression.
ita: Il migliore è consigliato se si ha molto tempo a disposizione e si desidera la migliore efficienza di compressione.
@@ -2393,7 +2736,7 @@ bframes:
spa: bframes
zho: b帧
'bframes: Maximum number of consecutive b-frames. ':
- deu: 'bframes: Maximale Anzahl von aufeinanderfolgenden b-Frames. '
+ deu: 'bframes: Maximale Anzahl aufeinanderfolgender b-Frames. '
eng: 'bframes: Maximum number of consecutive b-frames. '
fra: 'bframes : Nombre maximum de b-frames consécutives. '
ita: 'bframes: Numero massimo di b-frame consecutivi. '
@@ -2407,7 +2750,7 @@ but it has severe performance implications.:
spa: pero tiene severas implicaciones de rendimiento.
zho: 但对性能有严重影响。
but over a period of multiple frames instead of a single keyframe.:
- deu: aber über einen Zeitraum von mehreren Frames anstelle eines einzelnen Keyframes.
+ deu: aber über eine Dauer von mehreren Frames anstatt eines einzelnen Keyframes.
eng: but over a period of multiple frames instead of a single keyframe.
fra: mais sur une période de plusieurs images au lieu d'une seule image clé.
ita: ma su un periodo di frame multipli invece di un singolo keyframe.
@@ -2470,7 +2813,7 @@ data tracks found:
spa: 'Hilos de marcos: Número de cuadros codificados simultáneamente.'
zho: frame-threads:同时编码的帧数。
good is the default and recommended for most applications:
- deu: good ist der Standard und wird für die meisten Anwendungen empfohlen
+ deu: good ist die Standardeinstellung und wird für die meisten Anwendungszwecke empfohlen
eng: good is the default and recommended for most applications
fra: good est la valeur par défaut et est recommandé pour la plupart des applications
ita: buono è il valore predefinito e raccomandato per la maggior parte delle applicazioni
@@ -2490,6 +2833,13 @@ good is the default and recommended for most applications:
ita: 'hdr10: Forza la segnalazione dei parametri HDR10 nei pacchetti SEI.'
spa: 'hdr10: Forzar la señalización de los parámetros HDR10 en los paquetes SEI.'
zho: hdr10:强制在SEI包中发送HDR10参数。
+hq - High Quality, ll - Low Latency, ull - Ultra Low Latency:
+ deu: hq - Hohe Qualität, ll - niedrige Latenz, ull - extrem niedrige Latenz
+ eng: hq - High Quality, ll - Low Latency, ull - Ultra Low Latency
+ fra: hq - Haute qualité, ll - Latence faible, ull - Latence ultra faible
+ ita: hq - Alta qualità, ll - Bassa latenza, ull - Ultra bassa latenza
+ spa: hq - Alta calidad, ll - Baja latencia, ull - Ultra baja latencia
+ zho: hq - 高质量,ll - 低延迟,ull - 超低延迟。
installer:
deu: Installationsprogramm
eng: installer
@@ -2512,7 +2862,7 @@ is a default profile and will not be removed:
spa: es un perfil predeterminado y no se eliminará
zho: 是默认方案,不会被删除。
is extremely source dependant:
- deu: ist extrem quellenabhängig
+ deu: ist extrem abhängig von der Quelle
eng: is extremely source dependant
fra: est extrêmement dépendante de la source
ita: è estremamente dipendente dalla fonte
@@ -2526,7 +2876,7 @@ it will generally just increase memory use.:
spa: generalmente sólo aumentará el uso de la memoria.
zho: 一般只会增加内存占用。
'keyint: Enable Intra-Encoding by forcing keyframes every 1 second (Blu-ray spec)':
- deu: 'keyint: Aktiviert Intra-Encoding durch Erzwingen von Keyframes alle 1 Sekunde (Blu-ray-Spec)'
+ deu: 'keyint: Aktiviert Intra-Encoding durch Erzwingen von Keyframes alle 1 Sekunde (Blu-ray-Spezifikation)'
eng: 'keyint: Enable Intra-Encoding by forcing keyframes every 1 second (Blu-ray spec)'
fra: "keyint : Activer l'intra-encodage en forçant les images clés toutes les 1 seconde (spécification Blu-ray)"
ita: "keyint: Attivare l'Intra-Encoding forzando i keyframe ogni 1 secondo (Blu-ray spec)"
@@ -2540,7 +2890,7 @@ lossless:
spa: Lossless
zho: 无损
'max_muxing_queue_size: Raise to fix "Too many packets buffered for output stream" error':
- deu: 'max_muxing_queue_size: Erhöhen, um den Fehler "Zu viele Pakete für den Ausgabestrom gepuffert" zu beheben'
+ deu: 'max_muxing_queue_size: Erhöhen, um den Fehler "Too many packets buffered for output stream" zu beheben'
eng: 'max_muxing_queue_size: Raise to fix "Too many packets buffered for output stream" error'
fra: "max_muxing_queue_size : Augmenter pour corriger l'erreur \"Too many packets buffered for output stream"
ita: "max_muxing_queue_size: Alzare per correggere l'errore \"Troppi pacchetti bufferizzati per il flusso di uscita"
@@ -2561,14 +2911,14 @@ of:
spa: de
zho: 的
out file is already in queue:
- deu: out-Datei ist bereits in der Warteschlange
+ deu: Ausgabedatei ist bereits in der Warteschlange
eng: out file is already in queue
fra: notre dossier est déjà dans la file d'attente
ita: il file in uscita è già in coda
spa: nuestro archivo ya está en la cola
zho: out file is already in queue
portable:
- deu: portable
+ deu: portabel
eng: portable
fra: portable
ita: portatile
@@ -2582,7 +2932,7 @@ preset:
spa: preestablecido
zho: 预设
'preset: The slower the preset, the better the compression and quality':
- deu: 'voreingestellt: Je langsamer die Voreinstellung, desto besser die Komprimierung und Qualität'
+ deu: 'Voreinstellung: Je langsamer die Voreinstellung, desto besser die Komprimierung und Qualität'
eng: 'preset: The slower the preset, the better the compression and quality'
fra: 'préréglé : Plus le préréglage est lent, meilleure est la compression et la qualité'
ita: 'preimpostata: Più lento è il preset, migliore è la compressione e la qualità'
@@ -2603,7 +2953,7 @@ profile:
spa: perfil
zho: profile
'profile: Enforce an encode profile':
- deu: 'Profil: Erzwingt ein Kodierprofil'
+ deu: 'Profil: Erzwingt ein Kodierungsprofil'
eng: 'profile: Enforce an encode profile'
fra: 'profil : Appliquer un profil de codage'
ita: 'profilo: Applicare un profilo di codifica'
@@ -2631,7 +2981,7 @@ rav1e github:
spa: rav1e github
zho: rav1e github
'repeat-headers: If enabled, x265 will emit VPS, SPS, and PPS headers with every keyframe.':
- deu: 'Kopfzeilen wiederholeng: Wenn aktiviert, gibt x265 mit jedem Keyframe VPS-, SPS- und PPS-Header aus.'
+ deu: 'Kopfzeilen wiederholen: Wenn aktiviert, gibt x265 mit jedem Keyframe VPS-, SPS- und PPS-Header aus.'
eng: 'repeat-headers: If enabled, x265 will emit VPS, SPS, and PPS headers with every keyframe.'
fra: 'des en-têtes répétitifs : Si elle est activée, x265 émettra des en-têtes VPS, SPS et PPS avec chaque image clé.'
ita: 'ripeti-intestazioni: Se abilitato, x265 emetterà testate VPS, SPS e PPS con ogni fotogramma chiave.'
@@ -2659,14 +3009,14 @@ subtitle tracks found:
spa: pistas de subtítulos encontradas
zho: 找到字幕轨
that move across the video from one side to the other and thereby refresh the image:
- deu: die sich über das Video von einer Seite zur anderen bewegen und dabei das Bild aktualisieren
+ deu: die sich von einer Seite zur anderen durch das Video bewegen und dabei das Bild aktualisieren
eng: that move across the video from one side to the other and thereby refresh the image
fra: qui passent d'un côté à l'autre de la vidéo et rafraîchissent ainsi l'image
ita: che si muovono attraverso il video da un lato all'altro e quindi rinfrescano l'immagine
spa: que se mueven a través del video de un lado a otro y así refrescan la imagen
zho: 这些intra blocks的位置在若干帧的时间内从视频一侧移动到另一侧,
the resolution-to-:
- deu: die Reso
+ deu: die Auflösung zu
eng: the resolution-to-
fra: la reso
ita: il risuonare
@@ -2680,7 +3030,7 @@ to Blu-ray standards to burn to a physical disk:
spa: a los estándares de Blu-ray para grabar en un disco físico
zho: 否则不建议启用该选项。
'tune: Tune the settings for a particular type of source or situation':
- deu: 'abstimmen: Die Einstellungen für eine bestimmte Art von Quelle oder Situation abstimmen'
+ deu: 'Feineinstellung: Die Einstellungen auf eine bestimmte Art von Quelle oder Situation abstimmen'
eng: 'tune: Tune the settings for a particular type of source or situation'
fra: 'afinar: Sintonizar los ajustes para un tipo de fuente o situación particular'
ita: 'sintonizzarsi: Sintonizzare le impostazioni per un particolare tipo di sorgente o situazione'
@@ -2700,80 +3050,87 @@ vsync:
ita: vsync
spa: vsync
zho: vsync
-File:
- deu: Datei
- eng: File
- fra: Fichier
- ita: Archivio
- spa: Archivo
- zho: 文件
-Both Passes:
- deu: Beide Durchgänge
- eng: Both Passes
- fra: Les deux passages
- ita: Entrambi i Pass
- spa: Ambos pases
- zho: 两遍均应用
-Advanced settings are currently not saved in Profiles:
- deu: Erweiterte Einstellungen werden derzeit nicht in Profilen gespeichert
- eng: Advanced settings are currently not saved in Profiles
- fra: Les paramètres avancés ne sont actuellement pas enregistrés dans les Profils
- ita: Le impostazioni avanzate non sono attualmente salvate in Profili
- spa: Los ajustes avanzados no se guardan actualmente en Perfiles
- zho: 高级设置目前不会保存到方案中
-Constant:
- deu: Konstant
- eng: Constant
- fra: Constant
- ita: Costante
- spa: Constante
- zho: 恒定
-Please make sure seek method is set to exact:
- deu: Bitte stellen Sie sicher, dass die Suchmethode auf exakt eingestellt ist
- eng: Please make sure seek method is set to exact
- fra: Veuillez vous assurer que la méthode de recherche est réglée sur l'exacte
- ita: Si prega di assicurarsi che il metodo di ricerca sia impostato su
- spa: Por favor, asegúrese de que el método de búsqueda se establece con exactitud
- zho: 请确保检索方式已设置为exact
-Extract:
- deu: Auszug
- eng: Extract
- fra: Extrait
- ita: Estratto
- spa: Extracto
- zho: 提取
-Save File:
- deu: Datei speichern
- eng: Save File
- fra: Enregistrer le fichier
- ita: Salva file
- spa: Guardar archivo
- zho: 保存文件
-GUI Logging Level:
- deu: GUI-Protokollierungsebene
- eng: GUI Logging Level
- fra: Niveau d'exploitation forestière
- ita: Livello di registrazione GUI
- spa: Nivel de registro GUI
- zho: GUI日志级别
-Enable VBV:
- deu: VBV freigeben
- eng: Enable VBV
- fra: Activer la VBV
- ita: Attivare VBV
- spa: Activar VBV
- zho: 启用VBV
-Maxrate:
- deu: Maxrate
- eng: Maxrate
- fra: Maxrate
- ita: Maxrate
- spa: Maxrate
- zho: Maxrate
-Bufsize:
- deu: Bufsize
- eng: Bufsize
- fra: Bufsize
- ita: Bufsize
- spa: Bufsize
- zho: Bufsize
+There are no videos to start converting:
+ deu: Es sind keine Videos vorhanden, die konvertiert werden können
+ eng: There are no videos to start converting
+ fra: Il n'y a pas de vidéos à convertir
+ ita: Non ci sono video da convertire
+ spa: No hay vídeos para empezar a convertir
+ zho: 没有视频可以开始转换
+No crop, scale, rotation,flip nor any other filters will be applied.:
+ deu: Es werden weder Beschneiden, Skalieren, Drehen, Spiegeln noch andere Filter angewendet.
+ eng: No crop, scale, rotation,flip nor any other filters will be applied.
+ fra: Aucun filtre de culture, d'échelle, de rotation, de retournement ou autre ne sera appliqué.
+ ita: Nessun ritaglio, scala, rotazione, flip o qualsiasi altro filtro sarà applicato.
+ spa: No se aplicará ningún filtro de recorte, escala, rotación, volteo ni ningún otro.
+ zho: 不会应用裁剪、缩放、旋转、翻转或任何其他滤镜。
+Are you sure you want to stop the current encode?:
+ deu: Sind Sie sicher, dass Sie die aktuelle Kodierung stoppen wollen?
+ eng: Are you sure you want to stop the current encode?
+ fra: Êtes-vous sûr de vouloir arrêter le codage actuel?
+ ita: Sei sicuro di voler fermare la codifica in corso?
+ spa: ¿Está seguro de que quiere detener la codificación actual?
+ zho: 你确定要停止当前的编码吗?
+Confirm Stop Encode:
+ deu: Bestätigen Sie Stop Encode
+ eng: Confirm Stop Encode
+ fra: Confirmer le code d'arrêt
+ ita: Confermare Stop Encode
+ spa: Confirmar parada de codificación
+ zho: 确认 停止 编码
+Use Sane Audio Selection (updatable in config file):
+ deu: Audioauswahl verwenden (aktualisierbar in der Konfigurationsdatei)
+ eng: Use Sane Audio Selection (updatable in config file)
+ fra: Utiliser la sélection audio (mise à jour dans le fichier de configuration)
+ ita: Usa la selezione audio (aggiornabile nel file di configurazione)
+ spa: Utilizar la selección de audio (actualizable en el archivo de configuración)
+ zho: 使用音频选择(可在配置文件中更新)
+HDR10+ Parser:
+ deu: HDR10+ Parser
+ eng: HDR10+ Parser
+ fra: HDR10+ Parser
+ ita: HDR10+ Parser
+ spa: Analizador HDR10+
+ zho: HDR10+解析器
+Not all items in the queue were completed:
+ deu: Nicht alle Elemente in der Warteschlange wurden abgeschlossen
+ eng: Not all items in the queue were completed
+ fra: Tous les articles dans la file d'attente n'ont pas été complétés
+ ita: Non tutti gli elementi in coda sono stati completati
+ spa: No se han completado todos los elementos de la cola
+ zho: 并非队列中的所有项目都已完成
+Would you like to keep them in the queue?:
+ deu: Möchten Sie sie in der Warteschlange behalten?
+ eng: Would you like to keep them in the queue?
+ fra: Souhaitez-vous les garder dans la file d'attente?
+ ita: Volete tenerli in coda?
+ spa: ¿Quiere mantenerlos en la cola?
+ zho: 你想把他们留在队列中吗?
+Recover Queue Items:
+ deu: Warteschlangenelemente wiederherstellen
+ eng: Recover Queue Items
+ fra: Récupérer les articles en file d'attente
+ ita: Recuperare elementi della coda
+ spa: Recuperar elementos de la cola
+ zho: 恢复队列中的项目
+There is already a video being processed:
+ deu: Es ist bereits ein Video in Bearbeitung
+ eng: There is already a video being processed
+ fra: Il y a déjà une vidéo en cours de traitement
+ ita: C'è già un video in elaborazione
+ spa: Ya hay un vídeo en proceso
+ zho: 已经有一个视频正在处理中
+Are you sure you want to discard it?:
+ deu: Sind Sie sicher, dass Sie es verwerfen wollen?
+ eng: Are you sure you want to discard it?
+ fra: Êtes-vous sûr de vouloir la supprimer?
+ ita: Sei sicuro di volerlo scartare?
+ spa: ¿Estás seguro de que quieres descartarlo?
+ zho: 您确定要丢弃它吗?
+Discard current video:
+ deu: Aktuelles Video verwerfen
+ eng: Discard current video
+ fra: Jeter la vidéo actuelle
+ ita: Scartare il video corrente
+ spa: Descartar el vídeo actual
+ zho: 丢弃当前视频
diff --git a/fastflix/data/styles/default.qss b/fastflix/data/styles/default.qss
index 2055fd5a..174b9cd7 100644
--- a/fastflix/data/styles/default.qss
+++ b/fastflix/data/styles/default.qss
@@ -8,7 +8,7 @@ QPushButton {
min-height: 20px;
}
-QPushButton:pressed, QPushButton:clicked {
+QPushButton:pressed, QPushButton:clicked, QPushButton:checked {
background-color: #ccc;
}
@@ -56,3 +56,55 @@ QGroupBox {
margin-top: 2px;
border-bottom: 2px solid #ddd;
}
+
+/* QSlider::groove:horizontal {*/
+/*border: 1px solid #bbb;*/
+/*background: none;*/
+/*height: 10px;*/
+/*border-radius: 4px;*/
+/*}*/
+
+/*QSlider::sub-page:horizontal {*/
+/*background: none;*/
+/*border: 1px solid #bbb;*/
+/*height: 10px;*/
+/*border-radius: 4px;*/
+/*}*/
+
+/*QSlider::add-page:horizontal {*/
+/*background: none;*/
+/*border: 1px solid #bbb;*/
+/*height: 10px;*/
+/*border-radius: 4px;*/
+/*}*/
+
+/*QSlider::handle:horizontal {*/
+/*background: qlineargradient(x1:0, y1:0, x2:1, y2:1,stop:0 #eee, stop:1 #aaa);*/
+/*border: 1px solid #777;*/
+/*width: 13px;*/
+/*margin-top: -2px;*/
+/*margin-bottom: -2px;*/
+/*border-radius: 4px;*/
+/*}*/
+
+/*QSlider::handle:horizontal:hover {*/
+/*background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #fff, stop:1 #ddd);*/
+/*border: 1px solid #444;*/
+/*border-radius: 4px;*/
+/*}*/
+
+/*QSlider::sub-page:horizontal:disabled {*/
+/*background: #bbb;*/
+/*border-color: #999;*/
+/*}*/
+
+/*QSlider::add-page:horizontal:disabled {*/
+/*background: #eee;*/
+/*border-color: #999;*/
+/*}*/
+
+/*QSlider::handle:horizontal:disabled {*/
+/*background: #eee;*/
+/*border: 1px solid #aaa;*/
+/*border-radius: 4px;*/
+/*}*/
diff --git a/fastflix/encoders/av1_aom/command_builder.py b/fastflix/encoders/av1_aom/command_builder.py
index b0c1c985..f4cffa0f 100644
--- a/fastflix/encoders/av1_aom/command_builder.py
+++ b/fastflix/encoders/av1_aom/command_builder.py
@@ -23,8 +23,6 @@ def build(fastflix: FastFlix):
if settings.row_mt.lower() == "enabled":
beginning += f"-row-mt 1 "
- beginning = re.sub("[ ]+", " ", beginning)
-
if settings.bitrate:
pass_log_file = fastflix.current_video.work_path / f"pass_log_file_{secrets.token_hex(10)}"
command_1 = f'{beginning} -passlogfile "{pass_log_file}" -b:v {settings.bitrate} -pass 1 {settings.extra if settings.extra_both_passes else ""} -an -f matroska {null}'
diff --git a/fastflix/encoders/avc_x264/command_builder.py b/fastflix/encoders/avc_x264/command_builder.py
index d32a2be5..02d735a9 100644
--- a/fastflix/encoders/avc_x264/command_builder.py
+++ b/fastflix/encoders/avc_x264/command_builder.py
@@ -12,7 +12,7 @@ def build(fastflix: FastFlix):
beginning, ending = generate_all(fastflix, "libx264")
- beginning += f'{f"-tune {settings.tune}" if settings.tune else ""} ' f"{generate_color_details(fastflix)} "
+ beginning += f'{f"-tune:v {settings.tune}" if settings.tune else ""} {generate_color_details(fastflix)} '
if settings.profile and settings.profile != "default":
beginning += f"-profile:v {settings.profile} "
@@ -22,20 +22,20 @@ def build(fastflix: FastFlix):
if settings.bitrate:
command_1 = (
f"{beginning} -pass 1 "
- f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset {settings.preset} {settings.extra if settings.extra_both_passes else ""} -an -sn -dn -f mp4 {null}'
+ f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra if settings.extra_both_passes else ""} -an -sn -dn -f mp4 {null}'
)
command_2 = (
f'{beginning} -pass 2 -passlogfile "{pass_log_file}" '
- f"-b:v {settings.bitrate} -preset {settings.preset} {settings.extra} "
+ f"-b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra} "
) + ending
return [
- Command(command=re.sub("[ ]+", " ", command_1), name="First pass bitrate", exe="ffmpeg"),
- Command(command=re.sub("[ ]+", " ", command_2), name="Second pass bitrate", exe="ffmpeg"),
+ Command(command=command_1, name="First pass bitrate", exe="ffmpeg"),
+ Command(command=command_2, name="Second pass bitrate", exe="ffmpeg"),
]
elif settings.crf:
- command = f"{beginning} -crf {settings.crf} " f"-preset {settings.preset} {settings.extra} {ending}"
- return [Command(command=re.sub("[ ]+", " ", command), name="Single pass CRF", exe="ffmpeg")]
+ command = f"{beginning} -crf:v {settings.crf} " f"-preset:v {settings.preset} {settings.extra} {ending}"
+ return [Command(command=command, name="Single pass CRF", exe="ffmpeg")]
else:
return []
diff --git a/fastflix/encoders/avc_x264/settings_panel.py b/fastflix/encoders/avc_x264/settings_panel.py
index b2bf9f39..90b5a3d8 100644
--- a/fastflix/encoders/avc_x264/settings_panel.py
+++ b/fastflix/encoders/avc_x264/settings_panel.py
@@ -36,14 +36,14 @@
"25",
"24",
"23 (x264 default)",
- "22",
- "21",
- "20",
- "19 (480p)",
- "18 (720p)",
- "17 (1080p)",
- "16 (1440p)",
- "15 (2160p)",
+ "22 (1080p)",
+ "21 (1440p)",
+ "20 (2160p)",
+ "19",
+ "18",
+ "17",
+ "16",
+ "15",
"14 (higher quality)",
"Custom",
]
diff --git a/fastflix/encoders/common/attachments.py b/fastflix/encoders/common/attachments.py
index 16cbfc98..b83d2b3f 100644
--- a/fastflix/encoders/common/attachments.py
+++ b/fastflix/encoders/common/attachments.py
@@ -3,6 +3,7 @@
from typing import List
from fastflix.models.encode import AttachmentTrack
+from fastflix.shared import clean_file_string
def image_type(file: Path):
@@ -20,9 +21,9 @@ def build_attachments(attachments: List[AttachmentTrack]) -> str:
for attachment in attachments:
if attachment.attachment_type == "cover":
mime_type, ext_type = image_type(attachment.file_path)
- unixy_path = str(attachment.file_path).replace("\\", "/")
+ clean_path = clean_file_string(attachment.file_path)
commands.append(
- f' -attach "{unixy_path}" -metadata:s:{attachment.outdex} mimetype="{mime_type}" '
+ f' -attach "{clean_path}" -metadata:s:{attachment.outdex} mimetype="{mime_type}" '
f'-metadata:s:{attachment.outdex} filename="{attachment.filename}.{ext_type}" '
)
return " ".join(commands)
diff --git a/fastflix/encoders/common/audio.py b/fastflix/encoders/common/audio.py
index 11fa46d7..62c863e2 100644
--- a/fastflix/encoders/common/audio.py
+++ b/fastflix/encoders/common/audio.py
@@ -1,13 +1,35 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
+channel_list = {
+ "mono": 1,
+ "stereo": 2,
+ "2.1": 3,
+ "3.0": 3,
+ "3.0(back)": 3,
+ "3.1": 4,
+ "4.0": 4,
+ "quad": 4,
+ "quad(side)": 4,
+ "5.0": 5,
+ "5.1": 6,
+ "6.0": 6,
+ "6.0(front)": 6,
+ "hexagonal": 6,
+ "6.1": 7,
+ "6.1(front)": 7,
+ "7.0": 7,
+ "7.0(front)": 7,
+ "7.1": 8,
+ "7.1(wide)": 8,
+}
+
lossless = ["flac", "truehd", "alac", "tta", "wavpack", "mlp"]
def build_audio(audio_tracks, audio_file_index=0):
command_list = []
for track in audio_tracks:
- downmix = f"-ac:{track.outdex} {track.downmix}" if track.downmix > 0 else ""
command_list.append(
f"-map {audio_file_index}:{track.index} "
f'-metadata:s:{track.outdex} title="{track.title}" '
@@ -18,6 +40,11 @@ def build_audio(audio_tracks, audio_file_index=0):
if not track.conversion_codec or track.conversion_codec == "none":
command_list.append(f"-c:{track.outdex} copy")
elif track.conversion_codec:
+ downmix = (
+ f"-ac:{track.outdex} {channel_list[track.downmix]} -filter:{track.outdex} aformat=channel_layouts={track.downmix}"
+ if track.downmix
+ else ""
+ )
bitrate = ""
if track.conversion_codec not in lossless:
bitrate = f"-b:{track.outdex} {track.conversion_bitrate} "
diff --git a/fastflix/encoders/common/helpers.py b/fastflix/encoders/common/helpers.py
index bb95d461..3aa32aa7 100644
--- a/fastflix/encoders/common/helpers.py
+++ b/fastflix/encoders/common/helpers.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import uuid
from pathlib import Path
-from typing import List, Tuple, Union
+from typing import List, Tuple, Union, Optional, Dict
import reusables
from pydantic import BaseModel, Field
@@ -9,31 +9,18 @@
from fastflix.encoders.common.attachments import build_attachments
from fastflix.encoders.common.audio import build_audio
from fastflix.encoders.common.subtitles import build_subtitle
-from fastflix.models.base import BaseDataClass
from fastflix.models.fastflix import FastFlix
+from fastflix.shared import clean_file_string, sanitize
null = "/dev/null"
if reusables.win_based:
null = "NUL"
-class Loop:
- item = "loop"
-
- def __init__(self, condition, commands, dirs=(), files=(), name="", ensure_paths=()):
- self.name = name
- self.condition = condition
- self.commands = commands
- self.ensure_paths = ensure_paths
- self.dirs = dirs
- self.files = files
-
-
class Command(BaseModel):
command: str
item = "command"
name: str = ""
- ensure_paths: List = ()
exe: str = None
shell: bool = False
uuid: str = Field(default_factory=lambda: str(uuid.uuid4()))
@@ -63,8 +50,8 @@ def generate_ffmpeg_start(
incoming_fps = f"-r {source_fps}" if source_fps else ""
vsync_text = f"-vsync {vsync}" if vsync else ""
title = f'-metadata title="{video_title}"' if video_title else ""
- source = str(source).replace("\\", "/")
- ffmpeg = str(ffmpeg).replace("\\", "/")
+ source = clean_file_string(source)
+ ffmpeg = clean_file_string(ffmpeg)
return " ".join(
[
@@ -106,8 +93,7 @@ def generate_ending(
f"{audio} {subtitles} {cover} "
)
if output_video and not null_ending:
- output_video = str(output_video).replace("\\", "/")
- ending += f'"{output_video}"'
+ ending += f'"{clean_file_string(sanitize(output_video))}"'
else:
ending += null
return ending
@@ -116,11 +102,9 @@ def generate_ending(
def generate_filters(
selected_track,
source=None,
- crop=None,
+ crop: Optional[Dict] = None,
scale=None,
scale_filter="lanczos",
- scale_width=None,
- scale_height=None,
remove_hdr=False,
rotate=0,
vertical_flip=None,
@@ -128,6 +112,7 @@ def generate_filters(
burn_in_subtitle_track=None,
burn_in_subtitle_type=None,
custom_filters=None,
+ start_filters=None,
raw_filters=False,
deinterlace=False,
tone_map: str = "hable",
@@ -139,21 +124,21 @@ def generate_filters(
):
filter_list = []
+ if start_filters:
+ filter_list.append(start_filters)
if deinterlace:
filter_list.append(f"yadif")
if crop:
- filter_list.append(f"crop={crop}")
+ filter_list.append(f"crop={crop['width']}:{crop['height']}:{crop['left']}:{crop['top']}")
if scale:
filter_list.append(f"scale={scale}:flags={scale_filter}")
- elif scale_width:
- filter_list.append(f"scale={scale_width}:-8:flags={scale_filter}")
- elif scale_height:
- filter_list.append(f"scale=-8:{scale_height}:flags={scale_filter}")
if rotate:
- if rotate < 3:
- filter_list.append(f"transpose={rotate}")
- if rotate == 4:
+ if rotate == 1:
+ filter_list.append(f"transpose=1")
+ if rotate == 2:
filter_list.append(f"transpose=2,transpose=2")
+ if rotate == 3:
+ filter_list.append(f"transpose=2")
if vertical_flip:
filter_list.append("vflip")
if horizontal_flip:
@@ -183,8 +168,7 @@ def generate_filters(
else:
filter_complex = f"[0:{selected_track}][0:{burn_in_subtitle_track}]overlay[v]"
else:
- unixy = str(source).replace("\\", "/")
- filter_complex = f"[0:{selected_track}]{f'{filters},' if filters else ''}subtitles='{unixy}':si={burn_in_subtitle_track}[v]"
+ filter_complex = f"[0:{selected_track}]{f'{filters},' if filters else ''}subtitles='{clean_file_string(source)}':si={burn_in_subtitle_track}[v]"
elif filters:
filter_complex = f"[0:{selected_track}]{filters}[v]"
else:
diff --git a/fastflix/encoders/common/nvencc_helpers.py b/fastflix/encoders/common/nvencc_helpers.py
new file mode 100644
index 00000000..8a445d8c
--- /dev/null
+++ b/fastflix/encoders/common/nvencc_helpers.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+from typing import List, Dict
+import logging
+
+from fastflix.models.video import SubtitleTrack, AudioTrack
+from fastflix.encoders.common.audio import lossless
+
+
+logger = logging.getLogger("fastflix")
+
+
+def get_stream_pos(streams) -> Dict:
+ return {x.index: i for i, x in enumerate(streams, start=1)}
+
+
+def build_audio(audio_tracks: List[AudioTrack], audio_streams):
+ command_list = []
+ copies = []
+ track_ids = set()
+ stream_ids = get_stream_pos(audio_streams)
+
+ for track in sorted(audio_tracks, key=lambda x: x.outdex):
+ if track.index in track_ids:
+ logger.warning("NVEncC does not support copy and duplicate of audio tracks!")
+ track_ids.add(track.index)
+ audio_id = stream_ids[track.index]
+ if track.language:
+ command_list.append(f"--audio-metadata {audio_id}?language={track.language}")
+ if not track.conversion_codec or track.conversion_codec == "none":
+ copies.append(str(audio_id))
+ elif track.conversion_codec:
+ downmix = f"--audio-stream {audio_id}?:{track.downmix}" if track.downmix else ""
+ bitrate = ""
+ if track.conversion_codec not in lossless:
+ bitrate = f"--audio-bitrate {audio_id}?{track.conversion_bitrate.rstrip('k')} "
+ command_list.append(
+ f"{downmix} --audio-codec {audio_id}?{track.conversion_codec} {bitrate} "
+ f"--audio-metadata {audio_id}?clear"
+ )
+
+ if track.title:
+ command_list.append(
+ f'--audio-metadata {audio_id}?title="{track.title}" '
+ f'--audio-metadata {audio_id}?handler="{track.title}" '
+ )
+
+ return f" --audio-copy {','.join(copies)} {' '.join(command_list)}" if copies else f" {' '.join(command_list)}"
+
+
+def build_subtitle(subtitle_tracks: List[SubtitleTrack], subtitle_streams) -> str:
+ command_list = []
+ copies = []
+ stream_ids = get_stream_pos(subtitle_streams)
+
+ for track in sorted(subtitle_tracks, key=lambda x: x.outdex):
+ sub_id = stream_ids[track.index]
+ if track.burn_in:
+ command_list.append(f"--vpp-subburn track={sub_id}")
+ else:
+ copies.append(str(sub_id))
+ if track.disposition:
+ command_list.append(f"--sub-disposition {sub_id}?{track.disposition}")
+ command_list.append(f"--sub-metadata {sub_id}?language='{track.language}'")
+
+ return f" --sub-copy {','.join(copies)} {' '.join(command_list)}" if copies else f" {' '.join(command_list)}"
diff --git a/fastflix/encoders/common/setting_panel.py b/fastflix/encoders/common/setting_panel.py
index a2aa35ad..f4e20df2 100644
--- a/fastflix/encoders/common/setting_panel.py
+++ b/fastflix/encoders/common/setting_panel.py
@@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-
import logging
from typing import List, Tuple, Union
+from pathlib import Path
from box import Box
-from qtpy import QtGui, QtWidgets
+from qtpy import QtGui, QtWidgets, QtCore
from fastflix.exceptions import FastFlixInternalException
from fastflix.language import t
from fastflix.models.fastflix_app import FastFlixApp
+from fastflix.widgets.background_tasks import ExtractHDR10
logger = logging.getLogger("fastflix")
@@ -37,12 +39,15 @@ def determine_default(self, widget_name, opt, items: List, raise_error: bool = F
elif widget_name in ("crf", "qp"):
if not opt:
return 6
- items = [x.split("(")[0].split("-")[0].strip() for x in items]
opt = str(opt)
+ items = [x.split("(")[0].split("-")[0].strip() for x in items]
elif widget_name == "bitrate":
if not opt:
return 5
items = [x.split("(")[0].split("-")[0].strip() for x in items]
+ elif widget_name == "gpu":
+ if opt == -1:
+ return 0
if isinstance(opt, str):
try:
return items.index(opt)
@@ -50,22 +55,34 @@ def determine_default(self, widget_name, opt, items: List, raise_error: bool = F
if raise_error:
raise FastFlixInternalException
else:
- logger.error(f"Could not set default for {widget_name} to {opt} as it's not in the list")
+ logger.error(f"Could not set default for {widget_name} to {opt} as it's not in the list: {items}")
return 0
if isinstance(opt, bool):
return int(opt)
return opt
def _add_combo_box(
- self, label, options, widget_name, opt=None, connect="default", enabled=True, default=0, tooltip=""
+ self,
+ options,
+ widget_name,
+ label=None,
+ opt=None,
+ connect="default",
+ enabled=True,
+ default=0,
+ tooltip="",
+ min_width=None,
):
layout = QtWidgets.QHBoxLayout()
- self.labels[widget_name] = QtWidgets.QLabel(t(label))
- if tooltip:
- self.labels[widget_name].setToolTip(self.translate_tip(tooltip))
+ if label:
+ self.labels[widget_name] = QtWidgets.QLabel(t(label))
+ if tooltip:
+ self.labels[widget_name].setToolTip(self.translate_tip(tooltip))
self.widgets[widget_name] = QtWidgets.QComboBox()
self.widgets[widget_name].addItems(options)
+ if min_width:
+ self.widgets[widget_name].setMinimumWidth(min_width)
if opt:
default = self.determine_default(
@@ -86,6 +103,57 @@ def _add_combo_box(
else:
self.widgets[widget_name].currentIndexChanged.connect(connect)
+ if not label:
+ return self.widgets[widget_name]
+
+ layout.addWidget(self.labels[widget_name])
+ layout.addWidget(self.widgets[widget_name])
+
+ return layout
+
+ def _add_text_box(
+ self,
+ label,
+ widget_name,
+ opt=None,
+ connect="default",
+ enabled=True,
+ default="",
+ tooltip="",
+ validator=None,
+ width=None,
+ ):
+ layout = QtWidgets.QHBoxLayout()
+ self.labels[widget_name] = QtWidgets.QLabel(t(label))
+ if tooltip:
+ self.labels[widget_name].setToolTip(self.translate_tip(tooltip))
+
+ self.widgets[widget_name] = QtWidgets.QLineEdit()
+
+ if opt:
+ default = str(self.app.fastflix.config.encoder_opt(self.profile_name, opt)) or default
+ self.opts[widget_name] = opt
+ self.widgets[widget_name].setText(default)
+ self.widgets[widget_name].setDisabled(not enabled)
+ if tooltip:
+ self.widgets[widget_name].setToolTip(self.translate_tip(tooltip))
+ if connect:
+ if connect == "default":
+ self.widgets[widget_name].textChanged.connect(lambda: self.main.page_update(build_thumbnail=False))
+ elif connect == "self":
+ self.widgets[widget_name].textChanged.connect(lambda: self.page_update())
+ else:
+ self.widgets[widget_name].textChanged.connect(connect)
+
+ if validator:
+ if validator == "int":
+ self.widgets[widget_name].setValidator(self.only_int)
+ if validator == "float":
+ self.widgets[widget_name].setValidator(self.only_int)
+
+ if width:
+ self.widgets[widget_name].setFixedWidth(width)
+
layout.addWidget(self.labels[widget_name])
layout.addWidget(self.widgets[widget_name])
@@ -93,8 +161,6 @@ def _add_combo_box(
def _add_check_box(self, label, widget_name, opt, connect="default", enabled=True, checked=True, tooltip=""):
layout = QtWidgets.QHBoxLayout()
- # self.labels[widget_name] = QtWidgets.QLabel()
- # self.labels[widget_name].setToolTip()
self.widgets[widget_name] = QtWidgets.QCheckBox(t(label))
self.opts[widget_name] = opt
@@ -115,9 +181,9 @@ def _add_check_box(self, label, widget_name, opt, connect="default", enabled=Tru
return layout
- def _add_custom(self, connect="default", disable_both_passes=False):
+ def _add_custom(self, title="Custom ffmpeg options", connect="default", disable_both_passes=False):
layout = QtWidgets.QHBoxLayout()
- self.labels.ffmpeg_options = QtWidgets.QLabel(t("Custom ffmpeg options"))
+ self.labels.ffmpeg_options = QtWidgets.QLabel(t(title))
self.labels.ffmpeg_options.setToolTip(t("Extra flags or options, cannot modify existing settings"))
layout.addWidget(self.labels.ffmpeg_options)
self.ffmpeg_extras_widget = QtWidgets.QLineEdit()
@@ -166,11 +232,42 @@ def _add_file_select(self, label, widget_name, button_action, connect="default",
layout.addWidget(button)
return layout
+ def extract_hdr10plus(self):
+ self.extract_button.hide()
+ self.extract_label.show()
+ self.movie.start()
+ # self.extracting_hdr10 = True
+ self.extract_thrad = ExtractHDR10(
+ self.app, self.main, signal=self.hdr10plus_signal, ffmpeg_signal=self.hdr10plus_ffmpeg_signal
+ )
+ self.extract_thrad.start()
+
+ def done_hdr10plus_extract(self, metadata: str):
+ self.extract_button.show()
+ self.extract_label.hide()
+ self.movie.stop()
+ if Path(metadata).exists():
+ self.widgets.hdr10plus_metadata.setText(metadata)
+ self.ffmpeg_level.setText("")
+
+ def dhdr10_update(self):
+ dirname = Path(self.widgets.hdr10plus_metadata.text()).parent
+ if not dirname.exists():
+ dirname = Path()
+ filename = QtWidgets.QFileDialog.getOpenFileName(
+ self, caption="hdr10_metadata", directory=str(dirname), filter="HDR10+ Metadata (*.json)"
+ )
+ if not filename or not filename[0]:
+ return
+ self.widgets.hdr10plus_metadata.setText(filename[0])
+ self.main.page_update()
+
def _add_modes(
self,
recommended_bitrates,
recommended_qps,
qp_name="crf",
+ add_qp=True,
):
self.recommended_bitrates = recommended_bitrates
self.recommended_qps = recommended_qps
@@ -237,7 +334,8 @@ def _add_modes(
custom_qp = True
self.widgets[qp_name].setCurrentText("Custom")
else:
- self.widgets[qp_name].setCurrentIndex(default_qp_index)
+ if default_qp_index is not None:
+ self.widgets[qp_name].setCurrentIndex(default_qp_index)
self.widgets[qp_name].currentIndexChanged.connect(lambda: self.mode_update())
self.widgets[f"custom_{qp_name}"] = QtWidgets.QLineEdit("30" if not custom_qp else str(qp_value))
@@ -264,6 +362,9 @@ def _add_modes(
layout.addWidget(qp_group_box, 0, 0)
layout.addWidget(bitrate_group_box, 1, 0)
+ if not add_qp:
+ qp_group_box.hide()
+
return layout
@property
@@ -273,7 +374,7 @@ def ffmpeg_extras(self):
def ffmpeg_extra_update(self):
global ffmpeg_extra_command
ffmpeg_extra_command = self.ffmpeg_extras_widget.text().strip()
- self.main.page_update()
+ self.main.page_update(build_thumbnail=False)
def new_source(self):
if not self.app.fastflix.current_video or not self.app.fastflix.current_video.streams:
@@ -281,6 +382,7 @@ def new_source(self):
def update_profile(self):
global ffmpeg_extra_command
+ logger.debug("Update profile called")
for widget_name, opt in self.opts.items():
if isinstance(self.widgets[widget_name], QtWidgets.QComboBox):
default = self.determine_default(
@@ -305,6 +407,7 @@ def update_profile(self):
pass
else:
if bitrate:
+ self.mode = "Bitrate"
self.qp_radio.setChecked(False)
self.bitrate_radio.setChecked(True)
for i, rec in enumerate(self.recommended_bitrates):
@@ -315,6 +418,7 @@ def update_profile(self):
self.widgets.bitrate.setCurrentText("Custom")
self.widgets.custom_bitrate.setText(bitrate.rstrip("kKmMgGbB"))
else:
+ self.mode = self.qp_name
self.qp_radio.setChecked(True)
self.bitrate_radio.setChecked(False)
qp = str(self.app.fastflix.config.encoder_opt(self.profile_name, self.qp_name))
@@ -340,6 +444,7 @@ def init_max_mux(self):
def reload(self):
"""This will reset the current settings to what is set in "current_video", useful for return from queue"""
global ffmpeg_extra_command
+ logger.debug("Update reload called")
self.updating_settings = True
for widget_name, opt in self.opts.items():
data = getattr(self.app.fastflix.current_video.video_settings.video_encoder_settings, opt)
@@ -352,15 +457,18 @@ def reload(self):
self.widgets[widget_name].setCurrentIndex(data)
else:
self.widgets[widget_name].setCurrentText(data)
+ # Do smart check for cleaning up stuff
+
elif isinstance(self.widgets[widget_name], QtWidgets.QCheckBox):
self.widgets[widget_name].setChecked(data)
elif isinstance(self.widgets[widget_name], QtWidgets.QLineEdit):
if widget_name == "x265_params":
data = ":".join(data)
- self.widgets[widget_name].setText(data or "")
+ self.widgets[widget_name].setText(str(data) or "")
if getattr(self, "qp_radio", None):
bitrate = getattr(self.app.fastflix.current_video.video_settings.video_encoder_settings, "bitrate", None)
if bitrate:
+ self.mode = "Bitrate"
self.qp_radio.setChecked(False)
self.bitrate_radio.setChecked(True)
for i, rec in enumerate(self.recommended_bitrates):
@@ -371,6 +479,7 @@ def reload(self):
self.widgets.bitrate.setCurrentText("Custom")
self.widgets.custom_bitrate.setText(bitrate.rstrip("k"))
else:
+ self.mode = self.qp_name
self.qp_radio.setChecked(True)
self.bitrate_radio.setChecked(False)
qp = str(getattr(self.app.fastflix.current_video.video_settings.video_encoder_settings, self.qp_name))
diff --git a/fastflix/encoders/copy/command_builder.py b/fastflix/encoders/copy/command_builder.py
index 7fa855a0..c2d4e1dc 100644
--- a/fastflix/encoders/copy/command_builder.py
+++ b/fastflix/encoders/copy/command_builder.py
@@ -11,11 +11,7 @@ def build(fastflix: FastFlix):
return [
Command(
- command=re.sub(
- "[ ]+",
- " ",
- f"{beginning} {fastflix.current_video.video_settings.video_encoder_settings.extra} {ending}",
- ),
+ command=f"{beginning} {fastflix.current_video.video_settings.video_encoder_settings.extra} {ending}",
name="No Video Encoding",
exe="ffmpeg",
)
diff --git a/fastflix/encoders/ffmpeg_hevc_nvenc/__init__.py b/fastflix/encoders/ffmpeg_hevc_nvenc/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/fastflix/encoders/ffmpeg_hevc_nvenc/command_builder.py b/fastflix/encoders/ffmpeg_hevc_nvenc/command_builder.py
new file mode 100644
index 00000000..5baad548
--- /dev/null
+++ b/fastflix/encoders/ffmpeg_hevc_nvenc/command_builder.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+import re
+import secrets
+
+from fastflix.encoders.common.helpers import Command, generate_all, generate_color_details, null
+from fastflix.models.encode import FFmpegNVENCSettings
+from fastflix.models.fastflix import FastFlix
+
+
+def build(fastflix: FastFlix):
+ settings: FFmpegNVENCSettings = fastflix.current_video.video_settings.video_encoder_settings
+
+ beginning, ending = generate_all(fastflix, "hevc_nvenc")
+
+ beginning += f'{f"-tune:v {settings.tune}" if settings.tune else ""} {generate_color_details(fastflix)} -spatial_aq:v {settings.spatial_aq} -tier:v {settings.tier} -rc-lookahead:v {settings.rc_lookahead} -gpu {settings.gpu} -b_ref_mode {settings.b_ref_mode} '
+
+ if settings.profile:
+ beginning += f"-profile:v {settings.profile} "
+
+ if settings.rc:
+ beginning += f"-rc:v {settings.rc} "
+
+ if settings.level:
+ beginning += f"-level:v {settings.level} "
+
+ pass_log_file = fastflix.current_video.work_path / f"pass_log_file_{secrets.token_hex(10)}"
+
+ command_1 = (
+ f"{beginning} -pass 1 "
+ f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset:v {settings.preset} -2pass 1 '
+ f'{settings.extra if settings.extra_both_passes else ""} -an -sn -dn -f mp4 {null}'
+ )
+ command_2 = (
+ f'{beginning} -pass 2 -passlogfile "{pass_log_file}" -2pass 1 '
+ f"-b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra} "
+ ) + ending
+ return [
+ Command(command=command_1, name="First pass bitrate", exe="ffmpeg"),
+ Command(command=command_2, name="Second pass bitrate", exe="ffmpeg"),
+ ]
diff --git a/fastflix/encoders/ffmpeg_hevc_nvenc/main.py b/fastflix/encoders/ffmpeg_hevc_nvenc/main.py
new file mode 100644
index 00000000..130825ba
--- /dev/null
+++ b/fastflix/encoders/ffmpeg_hevc_nvenc/main.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+__author__ = "Chris Griffith"
+from pathlib import Path
+
+import pkg_resources
+
+name = "HEVC (NVENC)"
+requires = "cuda-llvm"
+
+video_extension = "mkv"
+video_dimension_divisor = 1
+icon = str(Path(pkg_resources.resource_filename(__name__, f"../../data/encoders/icon_nvenc.png")).resolve())
+
+enable_subtitles = True
+enable_audio = True
+enable_attachments = True
+
+audio_formats = [
+ "aac",
+ "aac_mf",
+ "libfdk_aac",
+ "ac3",
+ "ac3_fixed",
+ "ac3_mf",
+ "adpcm_adx",
+ "g722",
+ "g726",
+ "g726le",
+ "adpcm_ima_qt",
+ "adpcm_ima_ssi",
+ "adpcm_ima_wav",
+ "adpcm_ms",
+ "adpcm_swf",
+ "adpcm_yamaha",
+ "alac",
+ "libopencore_amrnb",
+ "libvo_amrwbenc",
+ "aptx",
+ "aptx_hd",
+ "comfortnoise",
+ "dca",
+ "eac3",
+ "flac",
+ "g723_1",
+ "libgsm",
+ "libgsm_ms",
+ "libilbc",
+ "mlp",
+ "mp2",
+ "mp2fixed",
+ "libtwolame",
+ "mp3_mf",
+ "libmp3lame",
+ "nellymoser",
+ "opus",
+ "libopus",
+ "pcm_alaw",
+ "pcm_dvd",
+ "pcm_f32be",
+ "pcm_f32le",
+ "pcm_f64be",
+ "pcm_f64le",
+ "pcm_mulaw",
+ "pcm_s16be",
+ "pcm_s16be_planar",
+ "pcm_s16le",
+ "pcm_s16le_planar",
+ "pcm_s24be",
+ "pcm_s24daud",
+ "pcm_s24le",
+ "pcm_s24le_planar",
+ "pcm_s32be",
+ "pcm_s32le",
+ "pcm_s32le_planar",
+ "pcm_s64be",
+ "pcm_s64le",
+ "pcm_s8",
+ "pcm_s8_planar",
+ "pcm_u16be",
+ "pcm_u16le",
+ "pcm_u24be",
+ "pcm_u24le",
+ "pcm_u32be",
+ "pcm_u32le",
+ "pcm_u8",
+ "pcm_vidc",
+ "real_144",
+ "roq_dpcm",
+ "s302m",
+ "sbc",
+ "sonic",
+ "sonicls",
+ "libspeex",
+ "truehd",
+ "tta",
+ "vorbis",
+ "libvorbis",
+ "wavpack",
+ "wmav1",
+ "wmav2",
+]
+
+from fastflix.encoders.ffmpeg_hevc_nvenc.command_builder import build
+from fastflix.encoders.ffmpeg_hevc_nvenc.settings_panel import NVENC as settings_panel
diff --git a/fastflix/encoders/ffmpeg_hevc_nvenc/settings_panel.py b/fastflix/encoders/ffmpeg_hevc_nvenc/settings_panel.py
new file mode 100644
index 00000000..feff57b1
--- /dev/null
+++ b/fastflix/encoders/ffmpeg_hevc_nvenc/settings_panel.py
@@ -0,0 +1,290 @@
+# -*- coding: utf-8 -*-
+import logging
+
+from box import Box
+from qtpy import QtCore, QtWidgets
+
+from fastflix.encoders.common.setting_panel import SettingPanel
+from fastflix.language import t
+from fastflix.models.encode import FFmpegNVENCSettings
+from fastflix.models.fastflix_app import FastFlixApp
+from fastflix.shared import link
+from fastflix.exceptions import FastFlixInternalException
+
+logger = logging.getLogger("fastflix")
+
+
+presets = [
+ "slow",
+ "medium",
+ "fast",
+ "hp",
+ "hq",
+ "bd",
+ "ll",
+ "llhq",
+ "llhp",
+ "lossless",
+ "losslesshp",
+ "p1",
+ "p2",
+ "p3",
+ "p4",
+ "p5",
+ "p6",
+ "p7",
+]
+
+recommended_bitrates = [
+ "800k (320x240p @ 30fps)",
+ "1000k (640x360p @ 30fps)",
+ "1500k (640x480p @ 30fps)",
+ "2000k (1280x720p @ 30fps)",
+ "5000k (1280x720p @ 60fps)",
+ "6000k (1080p @ 30fps)",
+ "9000k (1080p @ 60fps)",
+ "15000k (1440p @ 30fps)",
+ "25000k (1440p @ 60fps)",
+ "35000k (2160p @ 30fps)",
+ "50000k (2160p @ 60fps)",
+ "Custom",
+]
+
+recommended_crfs = [
+ "28",
+ "27",
+ "26",
+ "25",
+ "24",
+ "23",
+ "22",
+ "21",
+ "20",
+ "19",
+ "18",
+ "17",
+ "16",
+ "15",
+ "14",
+ "Custom",
+]
+
+pix_fmts = ["8-bit: yuv420p", "10-bit: p010le"]
+
+
+class NVENC(SettingPanel):
+ profile_name = "ffmpeg_hevc_nvenc"
+
+ def __init__(self, parent, main, app: FastFlixApp):
+ super().__init__(parent, main, app)
+ self.main = main
+ self.app = app
+
+ grid = QtWidgets.QGridLayout()
+
+ self.widgets = Box(mode=None)
+
+ self.mode = "CRF"
+ self.updating_settings = False
+
+ grid.addLayout(self.init_modes(), 0, 2, 3, 4)
+ grid.addLayout(self._add_custom(), 10, 0, 1, 6)
+
+ grid.addLayout(self.init_preset(), 0, 0, 1, 2)
+ grid.addLayout(self.init_max_mux(), 1, 0, 1, 2)
+ grid.addLayout(self.init_tune(), 2, 0, 1, 2)
+ grid.addLayout(self.init_profile(), 3, 0, 1, 2)
+ grid.addLayout(self.init_pix_fmt(), 4, 0, 1, 2)
+ grid.addLayout(self.init_tier(), 5, 0, 1, 2)
+ grid.addLayout(self.init_rc(), 6, 0, 1, 2)
+ grid.addLayout(self.init_spatial_aq(), 7, 0, 1, 2)
+
+ a = QtWidgets.QHBoxLayout()
+ a.addLayout(self.init_rc_lookahead())
+ a.addStretch(1)
+ a.addLayout(self.init_level())
+ a.addStretch(1)
+ a.addLayout(self.init_gpu())
+ a.addStretch(1)
+ a.addLayout(self.init_b_ref_mode())
+ grid.addLayout(a, 3, 2, 1, 4)
+
+ grid.setRowStretch(9, 1)
+
+ # guide_label = QtWidgets.QLabel(
+ # link("https://trac.ffmpeg.org/wiki/Encode/H.264", t("FFMPEG AVC / H.264 Encoding Guide"))
+ # )
+ # guide_label.setAlignment(QtCore.Qt.AlignBottom)
+ # guide_label.setOpenExternalLinks(True)
+ # grid.addWidget(guide_label, 11, 0, 1, 6)
+
+ self.setLayout(grid)
+ self.hide()
+
+ def init_preset(self):
+ return self._add_combo_box(
+ label="Preset",
+ widget_name="preset",
+ options=presets,
+ tooltip=("preset: The slower the preset, the better the compression and quality"),
+ connect="default",
+ opt="preset",
+ )
+
+ def init_tune(self):
+ return self._add_combo_box(
+ label="Tune",
+ widget_name="tune",
+ tooltip="Tune the settings for a particular type of source or situation\nhq - High Quality, ll - Low Latency, ull - Ultra Low Latency",
+ options=["hq", "ll", "ull", "lossless"],
+ opt="tune",
+ )
+
+ def init_profile(self):
+ return self._add_combo_box(
+ label="Profile_encoderopt",
+ widget_name="profile",
+ tooltip="Enforce an encode profile",
+ options=["main", "main10", "rext"],
+ opt="profile",
+ )
+
+ def init_pix_fmt(self):
+ return self._add_combo_box(
+ label="Bit Depth",
+ tooltip="Pixel Format (requires at least 10-bit for HDR)",
+ widget_name="pix_fmt",
+ options=pix_fmts,
+ opt="pix_fmt",
+ )
+
+ def init_tier(self):
+ return self._add_combo_box(
+ label="Tier",
+ tooltip="Set the encoding tier",
+ widget_name="tier",
+ options=["main", "high"],
+ opt="tier",
+ )
+
+ def init_rc(self):
+ return self._add_combo_box(
+ label="Rate Control",
+ tooltip="Override the preset rate-control",
+ widget_name="rc",
+ options=[
+ "default",
+ "vbr",
+ "cbr",
+ "vbr_minqp",
+ "ll_2pass_quality",
+ "ll_2pass_size",
+ "vbr_2pass",
+ "cbr_ld_hq",
+ "cbr_hq",
+ "vbr_hq",
+ ],
+ opt="rc",
+ )
+
+ def init_spatial_aq(self):
+ return self._add_combo_box(
+ label="Spatial AQ",
+ tooltip="",
+ widget_name="spatial_aq",
+ options=["off", "on"],
+ opt="spatial_aq",
+ )
+
+ def init_rc_lookahead(self):
+ return self._add_text_box(
+ label="RC Lookahead",
+ tooltip="",
+ widget_name="rc_lookahead",
+ opt="rc_lookahead",
+ validator="int",
+ default="0",
+ width=30,
+ )
+
+ def init_level(self):
+ layout = self._add_combo_box(
+ label="Level",
+ tooltip="Set the encoding level restriction",
+ widget_name="level",
+ options=["auto", "1.0", "2.0", "2.1", "3.0", "3.1", "4.0", "4.1", "5.0", "5.1", "5.2", "6.0", "6.1", "6.2"],
+ opt="level",
+ )
+ self.widgets.level.setMinimumWidth(60)
+ return layout
+
+ def init_gpu(self):
+ layout = self._add_combo_box(
+ label="GPU",
+ tooltip="Selects which NVENC capable GPU to use. First GPU is 0, second is 1, and so on",
+ widget_name="gpu",
+ opt="gpu",
+ options=["any"] + [str(x) for x in range(8)],
+ )
+ self.widgets.gpu.setMinimumWidth(50)
+ return layout
+
+ def init_b_ref_mode(self):
+ layout = self._add_combo_box(
+ label="B Ref Mode",
+ tooltip="Use B frames as references",
+ widget_name="b_ref_mode",
+ opt="b_ref_mode",
+ options=["disabled", "each", "middle"],
+ )
+ self.widgets.gpu.setMinimumWidth(50)
+ return layout
+
+ def init_modes(self):
+ layout = self._add_modes(recommended_bitrates, recommended_crfs, qp_name="qp", add_qp=False)
+ self.qp_radio.setChecked(False)
+ self.bitrate_radio.setChecked(True)
+ self.qp_radio.setDisabled(True)
+ return layout
+
+ def mode_update(self):
+ self.widgets.custom_qp.setDisabled(self.widgets.qp.currentText() != "Custom")
+ self.widgets.custom_bitrate.setDisabled(self.widgets.bitrate.currentText() != "Custom")
+ self.main.build_commands()
+
+ def setting_change(self, update=True):
+ if self.updating_settings:
+ return
+ self.updating_settings = True
+
+ if update:
+ self.main.page_update()
+ self.updating_settings = False
+
+ def update_video_encoder_settings(self):
+ tune = self.widgets.tune.currentText()
+
+ settings = FFmpegNVENCSettings(
+ preset=self.widgets.preset.currentText().split("-")[0].strip(),
+ max_muxing_queue_size=self.widgets.max_mux.currentText(),
+ profile=self.widgets.profile.currentText(),
+ pix_fmt=self.widgets.pix_fmt.currentText().split(":")[1].strip(),
+ extra=self.ffmpeg_extras,
+ tune=tune.split("-")[0].strip(),
+ extra_both_passes=self.widgets.extra_both_passes.isChecked(),
+ rc=self.widgets.rc.currentText() if self.widgets.rc.currentIndex() != 0 else None,
+ spatial_aq=self.widgets.spatial_aq.currentIndex(),
+ rc_lookahead=int(self.widgets.rc_lookahead.text() or 0),
+ 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(),
+ tier=self.widgets.tier.currentText(),
+ )
+ encode_type, q_value = self.get_mode_settings()
+ settings.qp = q_value if encode_type == "qp" else None
+ settings.bitrate = q_value if encode_type == "bitrate" else None
+ self.app.fastflix.current_video.video_settings.video_encoder_settings = settings
+
+ def set_mode(self, x):
+ self.mode = x.text()
+ self.main.build_commands()
diff --git a/fastflix/encoders/gif/command_builder.py b/fastflix/encoders/gif/command_builder.py
index a4bc711d..95dfd88f 100644
--- a/fastflix/encoders/gif/command_builder.py
+++ b/fastflix/encoders/gif/command_builder.py
@@ -4,6 +4,7 @@
from fastflix.encoders.common.helpers import Command, generate_filters
from fastflix.models.encode import GIFSettings
from fastflix.models.fastflix import FastFlix
+from fastflix.shared import clean_file_string
def build(fastflix: FastFlix):
@@ -15,7 +16,7 @@ def build(fastflix: FastFlix):
custom_filters=f"fps={settings.fps:.2f}", raw_filters=True, **fastflix.current_video.video_settings.dict()
)
- output_video = str(fastflix.current_video.video_settings.output_path).replace("\\", "/")
+ output_video = clean_file_string(fastflix.current_video.video_settings.output_path)
beginning = (
f'"{fastflix.config.ffmpeg}" -y '
f'{f"-ss {fastflix.current_video.video_settings.start_time}" if fastflix.current_video.video_settings.start_time else ""} '
diff --git a/fastflix/encoders/hevc_x265/command_builder.py b/fastflix/encoders/hevc_x265/command_builder.py
index adf3c359..201c4523 100644
--- a/fastflix/encoders/hevc_x265/command_builder.py
+++ b/fastflix/encoders/hevc_x265/command_builder.py
@@ -74,6 +74,8 @@
color_matrix_mapping = {"bt2020_ncl": "bt2020nc", "bt2020_cl": "bt2020c"}
+chromaloc_mapping = {"left": 0, "center": 1, "topleft": 2, "top": 3, "bottomleft": 4, "bottom": 5}
+
def build(fastflix: FastFlix):
settings: x265Settings = fastflix.current_video.video_settings.video_encoder_settings
@@ -81,7 +83,7 @@ def build(fastflix: FastFlix):
beginning, ending = generate_all(fastflix, "libx265")
if settings.tune and settings.tune != "default":
- beginning += f"-tune {settings.tune} "
+ beginning += f"-tune:v {settings.tune} "
if settings.profile and settings.profile != "default":
beginning += f"-profile:v {settings.profile} "
@@ -134,6 +136,10 @@ def build(fastflix: FastFlix):
x265_params.append(f"hdr10={'1' if settings.hdr10 else '0'}")
+ current_chroma_loc = fastflix.current_video.current_video_stream.get("chroma_location")
+ if current_chroma_loc in chromaloc_mapping:
+ x265_params.append(f"chromaloc={chromaloc_mapping[current_chroma_loc]}")
+
if settings.hdr10plus_metadata:
x265_params.append(f"dhdr10-info='{settings.hdr10plus_metadata}'")
@@ -164,24 +170,24 @@ def get_x265_params(params=()):
if settings.bitrate:
command_1 = (
f'{beginning} {get_x265_params(["pass=1", "no-slow-firstpass=1"])} '
- f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset {settings.preset} {settings.extra if settings.extra_both_passes else ""} '
+ f'-passlogfile "{pass_log_file}" -b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra if settings.extra_both_passes else ""} '
f" -an -sn -dn -f mp4 {null}"
)
command_2 = (
f'{beginning} {get_x265_params(["pass=2"])} -passlogfile "{pass_log_file}" '
- f"-b:v {settings.bitrate} -preset {settings.preset} {settings.extra} {ending}"
+ f"-b:v {settings.bitrate} -preset:v {settings.preset} {settings.extra} {ending}"
)
return [
- Command(command=re.sub("[ ]+", " ", command_1), name="First pass bitrate", exe="ffmpeg"),
- Command(command=re.sub("[ ]+", " ", command_2), name="Second pass bitrate", exe="ffmpeg"),
+ Command(command=command_1, name="First pass bitrate", exe="ffmpeg"),
+ Command(command=command_2, name="Second pass bitrate", exe="ffmpeg"),
]
elif settings.crf:
command = (
- f"{beginning} {get_x265_params()} -crf {settings.crf} "
- f"-preset {settings.preset} {settings.extra} {ending}"
+ f"{beginning} {get_x265_params()} -crf:v {settings.crf} "
+ f"-preset:v {settings.preset} {settings.extra} {ending}"
)
- return [Command(command=re.sub("[ ]+", " ", command), name="Single pass CRF", exe="ffmpeg")]
+ return [Command(command=command, name="Single pass CRF", exe="ffmpeg")]
else:
return []
diff --git a/fastflix/encoders/hevc_x265/settings_panel.py b/fastflix/encoders/hevc_x265/settings_panel.py
index a88ec094..84dbcd13 100644
--- a/fastflix/encoders/hevc_x265/settings_panel.py
+++ b/fastflix/encoders/hevc_x265/settings_panel.py
@@ -10,8 +10,9 @@
from fastflix.language import t
from fastflix.models.encode import x265Settings
from fastflix.models.fastflix_app import FastFlixApp
-from fastflix.resources import warning_icon
+from fastflix.resources import loading_movie, warning_icon
from fastflix.shared import link
+from fastflix.widgets.background_tasks import ExtractHDR10
logger = logging.getLogger("fastflix")
@@ -63,6 +64,8 @@ def get_breaker():
class HEVC(SettingPanel):
profile_name = "x265"
+ hdr10plus_signal = QtCore.Signal(str)
+ hdr10plus_ffmpeg_signal = QtCore.Signal(str)
def __init__(self, parent, main, app: FastFlixApp):
super().__init__(parent, main, app)
@@ -73,6 +76,7 @@ def __init__(self, parent, main, app: FastFlixApp):
self.mode = "CRF"
self.updating_settings = False
+ self.extract_thread = None
grid.addLayout(self.init_preset(), 0, 0, 1, 2)
grid.addLayout(self.init_tune(), 1, 0, 1, 2)
@@ -103,10 +107,12 @@ def __init__(self, parent, main, app: FastFlixApp):
grid.addLayout(self.init_dhdr10_info(), 9, 2, 1, 3)
grid.addLayout(self.init_dhdr10_warning_and_opt(), 9, 5, 1, 1)
+ self.ffmpeg_level = QtWidgets.QLabel()
+ grid.addWidget(self.ffmpeg_level, 10, 2, 1, 4)
- grid.setRowStretch(10, True)
+ grid.setRowStretch(11, True)
- grid.addLayout(self._add_custom(), 11, 0, 1, 6)
+ grid.addLayout(self._add_custom(), 12, 0, 1, 6)
link_1 = link(
"https://trac.ffmpeg.org/wiki/Encode/H.265",
@@ -125,8 +131,10 @@ def __init__(self, parent, main, app: FastFlixApp):
guide_label.setAlignment(QtCore.Qt.AlignBottom)
guide_label.setOpenExternalLinks(True)
- grid.addWidget(guide_label, 12, 0, 1, 6)
+ grid.addWidget(guide_label, 13, 0, 1, 6)
+ self.hdr10plus_signal.connect(self.done_hdr10plus_extract)
+ self.hdr10plus_ffmpeg_signal.connect(lambda x: self.ffmpeg_level.setText(x))
self.setLayout(grid)
self.hide()
@@ -151,6 +159,20 @@ def init_dhdr10_warning_and_opt(self):
icon = QtGui.QIcon(warning_icon)
label.setPixmap(icon.pixmap(22))
layout = QtWidgets.QHBoxLayout()
+
+ self.extract_button = QtWidgets.QPushButton(t("Extract HDR10+"))
+ self.extract_button.hide()
+ self.extract_button.clicked.connect(self.extract_hdr10plus)
+
+ self.extract_label = QtWidgets.QLabel(self)
+ self.extract_label.hide()
+ self.movie = QtGui.QMovie(loading_movie)
+ self.movie.setScaledSize(QtCore.QSize(25, 25))
+ self.extract_label.setMovie(self.movie)
+
+ layout.addWidget(self.extract_button)
+ layout.addWidget(self.extract_label)
+
layout.addWidget(label)
layout.addLayout(self.init_dhdr10_opt())
return layout
@@ -432,18 +454,6 @@ def init_x265_params(self):
layout.addWidget(self.widgets.x265_params)
return layout
- def dhdr10_update(self):
- dirname = Path(self.widgets.hdr10plus_metadata.text()).parent
- if not dirname.exists():
- dirname = Path()
- filename = QtWidgets.QFileDialog.getOpenFileName(
- self, caption="hdr10_metadata", directory=str(dirname), filter="HDR10+ Metadata (*.json)"
- )
- if not filename or not filename[0]:
- return
- self.widgets.hdr10plus_metadata.setText(filename[0])
- self.main.page_update()
-
def setting_change(self, update=True, pix_change=False):
def hdr_opts():
if not self.widgets.pix_fmt.currentText().startswith(
@@ -509,8 +519,19 @@ def hdr_opts():
self.updating_settings = False
def new_source(self):
+ if not self.app.fastflix.current_video:
+ return
super().new_source()
self.setting_change()
+ if self.app.fastflix.current_video.hdr10_plus:
+ self.extract_button.show()
+ else:
+ self.extract_button.hide()
+ if self.extract_thread:
+ try:
+ self.extract_thread.terminate()
+ except Exception:
+ pass
def update_video_encoder_settings(self):
if not self.app.fastflix.current_video:
@@ -536,7 +557,7 @@ def update_video_encoder_settings(self):
frame_threads=self.widgets.frame_threads.currentIndex(),
tune=self.widgets.tune.currentText(),
x265_params=x265_params_text.split(":") if x265_params_text else [],
- hdr10plus_metadata=self.widgets.hdr10plus_metadata.text().strip().replace("\\", "/"),
+ hdr10plus_metadata=self.widgets.hdr10plus_metadata.text().strip(), # .replace("\\", "/"),
lossless=self.widgets.lossless.isChecked(),
extra=self.ffmpeg_extras,
extra_both_passes=self.widgets.extra_both_passes.isChecked(),
diff --git a/fastflix/encoders/nvencc_avc/__init__.py b/fastflix/encoders/nvencc_avc/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/fastflix/encoders/nvencc_avc/command_builder.py b/fastflix/encoders/nvencc_avc/command_builder.py
new file mode 100644
index 00000000..c3cf8f39
--- /dev/null
+++ b/fastflix/encoders/nvencc_avc/command_builder.py
@@ -0,0 +1,142 @@
+# -*- coding: utf-8 -*-
+import logging
+
+from fastflix.encoders.common.helpers import Command
+from fastflix.models.encode import NVEncCAVCSettings
+from fastflix.models.video import Video
+from fastflix.models.fastflix import FastFlix
+from fastflix.shared import clean_file_string
+from fastflix.encoders.common.nvencc_helpers import build_subtitle, build_audio
+
+logger = logging.getLogger("fastflix")
+
+
+def build(fastflix: FastFlix):
+ video: Video = fastflix.current_video
+ settings: NVEncCAVCSettings = fastflix.current_video.video_settings.video_encoder_settings
+
+ trim = ""
+ try:
+ if "/" in video.frame_rate:
+ over, under = [int(x) for x in video.frame_rate.split("/")]
+ rate = over / under
+ else:
+ rate = float(video.frame_rate)
+ except Exception:
+ logger.exception("Could not get framerate of this movie!")
+ else:
+ if video.video_settings.end_time:
+ end_frame = int(video.video_settings.end_time * rate)
+ start_frame = 0
+ if video.video_settings.start_time:
+ start_frame = int(video.video_settings.start_time * rate)
+ trim = f"--trim {start_frame}:{end_frame}"
+ elif video.video_settings.start_time:
+ trim = f"--seek {video.video_settings.start_time}"
+
+ if (video.frame_rate != video.average_frame_rate) and trim:
+ logger.warning("Cannot use 'trim' when working with variable frame rate videos")
+ trim = ""
+
+ transform = ""
+ if video.video_settings.vertical_flip or video.video_settings.horizontal_flip:
+ transform = f"--vpp-transform flip_x={'true' if video.video_settings.horizontal_flip else 'false'},flip_y={'true' if video.video_settings.vertical_flip else 'false'}"
+
+ remove_hdr = ""
+ if video.video_settings.remove_hdr:
+ remove_type = (
+ video.video_settings.tone_map
+ if video.video_settings.tone_map in ("mobius", "hable", "reinhard")
+ else "mobius"
+ )
+ remove_hdr = f"--vpp-colorspace hdr2sdr={remove_type}" if video.video_settings.remove_hdr else ""
+
+ crop = ""
+ if video.video_settings.crop:
+ crop = f"--crop {video.video_settings.crop.left},{video.video_settings.crop.top},{video.video_settings.crop.right},{video.video_settings.crop.bottom}"
+
+ vbv = ""
+ if video.video_settings.maxrate:
+ vbv = f"--max-bitrate {video.video_settings.maxrate} --vbv-bufsize {video.video_settings.bufsize}"
+
+ init_q = settings.init_q_i
+ if settings.init_q_i and settings.init_q_p and settings.init_q_b:
+ init_q = f"{settings.init_q_i}:{settings.init_q_p}:{settings.init_q_b}"
+
+ min_q = settings.min_q_i
+ if settings.min_q_i and settings.min_q_p and settings.min_q_b:
+ min_q = f"{settings.min_q_i}:{settings.min_q_p}:{settings.min_q_b}"
+
+ max_q = settings.max_q_i
+ if settings.max_q_i and settings.max_q_p and settings.max_q_b:
+ max_q = f"{settings.max_q_i}:{settings.max_q_p}:{settings.max_q_b}"
+
+ try:
+ stream_id = int(video.current_video_stream["id"], 16)
+ except Exception:
+ if len(video.streams.video) > 1:
+ logger.warning("Could not get stream ID from source, the proper video track may not be selected!")
+ stream_id = None
+
+ aq = "--no-aq"
+ if settings.aq.lower() == "spatial":
+ aq = f"--aq --aq-strength {settings.aq_strength}"
+ elif settings.aq.lower() == "temporal":
+ aq = f"--aq-temporal --aq-strength {settings.aq_strength}"
+
+ command = [
+ f'"{clean_file_string(fastflix.config.nvencc)}"',
+ "-i",
+ f'"{clean_file_string(video.source)}"',
+ (f"--video-streamid {stream_id}" if stream_id else ""),
+ trim,
+ (f"--vpp-rotate {video.video_settings.rotate * 90}" if video.video_settings.rotate else ""),
+ transform,
+ (f'--output-res {video.video_settings.scale.replace(":", "x")}' if video.video_settings.scale else ""),
+ crop,
+ (f"--video-metadata clear" if video.video_settings.remove_metadata else "--video-metadata copy"),
+ (f'--video-metadata title="{video.video_settings.video_title}"' if video.video_settings.video_title else ""),
+ ("--chapter-copy" if video.video_settings.copy_chapters else ""),
+ "-c",
+ "avc",
+ (f"--vbr {settings.bitrate.rstrip('k')}" if settings.bitrate else f"--cqp {settings.cqp}"),
+ vbv,
+ (f"--vbr-quality {settings.vbr_target}" if settings.vbr_target is not None and settings.bitrate else ""),
+ (f"--qp-init {init_q}" if init_q and settings.bitrate else ""),
+ (f"--qp-min {min_q}" if min_q and settings.bitrate else ""),
+ (f"--qp-max {max_q}" if max_q and settings.bitrate else ""),
+ (f"--bframes {settings.b_frames}" if settings.b_frames else ""),
+ (f"--ref {settings.ref}" if settings.ref else ""),
+ f"--bref-mode {settings.b_ref_mode}",
+ "--preset",
+ settings.preset,
+ (f"--lookahead {settings.lookahead}" if settings.lookahead else ""),
+ aq,
+ "--colormatrix",
+ (video.video_settings.color_space or "auto"),
+ "--transfer",
+ (video.video_settings.color_transfer or "auto"),
+ "--colorprim",
+ (video.video_settings.color_primaries or "auto"),
+ "--multipass",
+ settings.multipass,
+ "--mv-precision",
+ settings.mv_precision,
+ "--chromaloc",
+ "auto",
+ "--colorrange",
+ "auto",
+ f"--avsync {'cfr' if video.frame_rate == video.average_frame_rate else 'vfr'}",
+ (f"--interlace {video.interlaced}" if video.interlaced else ""),
+ ("--vpp-yadif" if video.video_settings.deinterlace else ""),
+ (f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""),
+ remove_hdr,
+ "--psnr --ssim" if settings.metrics else "",
+ build_audio(video.video_settings.audio_tracks, video.streams.audio),
+ build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle),
+ settings.extra,
+ "-o",
+ f'"{clean_file_string(video.video_settings.output_path)}"',
+ ]
+
+ return [Command(command=" ".join(x for x in command if x), name="NVEncC Encode", exe="NVEncE")]
diff --git a/fastflix/encoders/nvencc_avc/main.py b/fastflix/encoders/nvencc_avc/main.py
new file mode 100644
index 00000000..a6c34e95
--- /dev/null
+++ b/fastflix/encoders/nvencc_avc/main.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+__author__ = "Chris Griffith"
+from pathlib import Path
+
+import pkg_resources
+
+name = "AVC (NVEncC)"
+
+video_extension = "mkv"
+video_dimension_divisor = 1
+icon = str(Path(pkg_resources.resource_filename(__name__, f"../../data/encoders/icon_nvencc.png")).resolve())
+
+enable_subtitles = True
+enable_audio = True
+enable_attachments = False
+
+audio_formats = [
+ "aac",
+ "aac_mf",
+ "libfdk_aac",
+ "ac3",
+ "ac3_fixed",
+ "ac3_mf",
+ "adpcm_adx",
+ "g722",
+ "g726",
+ "g726le",
+ "adpcm_ima_qt",
+ "adpcm_ima_ssi",
+ "adpcm_ima_wav",
+ "adpcm_ms",
+ "adpcm_swf",
+ "adpcm_yamaha",
+ "alac",
+ "libopencore_amrnb",
+ "libvo_amrwbenc",
+ "aptx",
+ "aptx_hd",
+ "comfortnoise",
+ "dca",
+ "eac3",
+ "flac",
+ "g723_1",
+ "libgsm",
+ "libgsm_ms",
+ "libilbc",
+ "mlp",
+ "mp2",
+ "mp2fixed",
+ "libtwolame",
+ "mp3_mf",
+ "libmp3lame",
+ "nellymoser",
+ "opus",
+ "libopus",
+ "pcm_alaw",
+ "pcm_dvd",
+ "pcm_f32be",
+ "pcm_f32le",
+ "pcm_f64be",
+ "pcm_f64le",
+ "pcm_mulaw",
+ "pcm_s16be",
+ "pcm_s16be_planar",
+ "pcm_s16le",
+ "pcm_s16le_planar",
+ "pcm_s24be",
+ "pcm_s24daud",
+ "pcm_s24le",
+ "pcm_s24le_planar",
+ "pcm_s32be",
+ "pcm_s32le",
+ "pcm_s32le_planar",
+ "pcm_s64be",
+ "pcm_s64le",
+ "pcm_s8",
+ "pcm_s8_planar",
+ "pcm_u16be",
+ "pcm_u16le",
+ "pcm_u24be",
+ "pcm_u24le",
+ "pcm_u32be",
+ "pcm_u32le",
+ "pcm_u8",
+ "pcm_vidc",
+ "real_144",
+ "roq_dpcm",
+ "s302m",
+ "sbc",
+ "sonic",
+ "sonicls",
+ "libspeex",
+ "truehd",
+ "tta",
+ "vorbis",
+ "libvorbis",
+ "wavpack",
+ "wmav1",
+ "wmav2",
+]
+
+from fastflix.encoders.nvencc_avc.command_builder import build
+from fastflix.encoders.nvencc_avc.settings_panel import NVENCCAVC as settings_panel
diff --git a/fastflix/encoders/nvencc_avc/settings_panel.py b/fastflix/encoders/nvencc_avc/settings_panel.py
new file mode 100644
index 00000000..0954cd15
--- /dev/null
+++ b/fastflix/encoders/nvencc_avc/settings_panel.py
@@ -0,0 +1,441 @@
+# -*- coding: utf-8 -*-
+import logging
+from typing import List, Optional
+
+from box import Box
+from qtpy import QtCore, QtWidgets, QtGui
+
+from fastflix.encoders.common.setting_panel import SettingPanel
+from fastflix.language import t
+from fastflix.models.encode import NVEncCAVCSettings
+from fastflix.models.fastflix_app import FastFlixApp
+from fastflix.shared import link
+from fastflix.exceptions import FastFlixInternalException
+from fastflix.resources import loading_movie, warning_icon
+
+logger = logging.getLogger("fastflix")
+
+presets = ["default", "performance", "quality"]
+
+recommended_bitrates = [
+ "200k (320x240p @ 30fps)",
+ "300k (640x360p @ 30fps)",
+ "1000k (640x480p @ 30fps)",
+ "1750k (1280x720p @ 30fps)",
+ "2500k (1280x720p @ 60fps)",
+ "4000k (1920x1080p @ 30fps)",
+ "5000k (1920x1080p @ 60fps)",
+ "7000k (2560x1440p @ 30fps)",
+ "10000k (2560x1440p @ 60fps)",
+ "15000k (3840x2160p @ 30fps)",
+ "20000k (3840x2160p @ 60fps)",
+ "Custom",
+]
+
+recommended_crfs = [
+ "28",
+ "27",
+ "26",
+ "25",
+ "24",
+ "23",
+ "22",
+ "21",
+ "20",
+ "19",
+ "18",
+ "17",
+ "16",
+ "15",
+ "14",
+ "Custom",
+]
+
+
+def get_breaker():
+ breaker_line = QtWidgets.QWidget()
+ breaker_line.setMaximumHeight(2)
+ breaker_line.setStyleSheet("background-color: #ccc; margin: auto 0; padding: auto 0;")
+ return breaker_line
+
+
+class NVENCCAVC(SettingPanel):
+ profile_name = "nvencc_avc"
+ hdr10plus_signal = QtCore.Signal(str)
+ hdr10plus_ffmpeg_signal = QtCore.Signal(str)
+
+ def __init__(self, parent, main, app: FastFlixApp):
+ super().__init__(parent, main, app)
+ self.main = main
+ self.app = app
+
+ grid = QtWidgets.QGridLayout()
+
+ self.widgets = Box(mode=None)
+
+ self.mode = "Bitrate"
+ self.updating_settings = False
+
+ grid.addLayout(self.init_modes(), 0, 2, 4, 4)
+ grid.addLayout(self._add_custom(title="Custom NVEncC options", disable_both_passes=True), 10, 0, 1, 6)
+
+ grid.addLayout(self.init_preset(), 0, 0, 1, 2)
+ # grid.addLayout(self.init_profile(), 1, 0, 1, 2)
+ # grid.addLayout(self.init_tier(), 1, 0, 1, 2)
+ grid.addLayout(self.init_multipass(), 2, 0, 1, 2)
+ grid.addLayout(self.init_lookahead(), 3, 0, 1, 2)
+
+ breaker = QtWidgets.QHBoxLayout()
+ breaker_label = QtWidgets.QLabel(t("Advanced"))
+ breaker_label.setFont(QtGui.QFont("helvetica", 8, weight=55))
+
+ breaker.addWidget(get_breaker(), stretch=1)
+ breaker.addWidget(breaker_label, alignment=QtCore.Qt.AlignHCenter)
+ breaker.addWidget(get_breaker(), stretch=1)
+
+ grid.addLayout(breaker, 4, 0, 1, 6)
+
+ grid.addLayout(self.init_aq(), 5, 0, 1, 2)
+ grid.addLayout(self.init_aq_strength(), 6, 0, 1, 2)
+ grid.addLayout(self.init_mv_precision(), 7, 0, 1, 2)
+
+ qp_line = QtWidgets.QHBoxLayout()
+ qp_line.addLayout(self.init_vbr_target())
+ qp_line.addStretch(1)
+ qp_line.addLayout(self.init_init_q())
+ qp_line.addStretch(1)
+ qp_line.addLayout(self.init_min_q())
+ qp_line.addStretch(1)
+ qp_line.addLayout(self.init_max_q())
+
+ grid.addLayout(qp_line, 5, 2, 1, 4)
+
+ advanced = QtWidgets.QHBoxLayout()
+ advanced.addLayout(self.init_ref())
+ advanced.addStretch(1)
+ advanced.addLayout(self.init_b_frames())
+ advanced.addStretch(1)
+ advanced.addLayout(self.init_level())
+ advanced.addStretch(1)
+ advanced.addLayout(self.init_b_ref_mode())
+ advanced.addStretch(1)
+ advanced.addLayout(self.init_metrics())
+ grid.addLayout(advanced, 6, 2, 1, 4)
+
+ grid.addLayout(self.init_dhdr10_info(), 7, 2, 1, 4)
+
+ self.ffmpeg_level = QtWidgets.QLabel()
+ grid.addWidget(self.ffmpeg_level, 8, 2, 1, 4)
+
+ grid.setRowStretch(9, 1)
+
+ guide_label = QtWidgets.QLabel(
+ link("https://github.com/rigaya/NVEnc/blob/master/NVEncC_Options.en.md", t("NVEncC Options"))
+ )
+
+ warning_label = QtWidgets.QLabel()
+ warning_label.setPixmap(QtGui.QIcon(warning_icon).pixmap(22))
+
+ guide_label.setAlignment(QtCore.Qt.AlignBottom)
+ guide_label.setOpenExternalLinks(True)
+ grid.addWidget(guide_label, 11, 0, 1, 4)
+ grid.addWidget(warning_label, 11, 4, 1, 1, alignment=QtCore.Qt.AlignRight)
+ grid.addWidget(QtWidgets.QLabel(t("NVEncC Encoder support is still experimental!")), 11, 5, 1, 1)
+
+ self.setLayout(grid)
+ self.hide()
+ self.hdr10plus_signal.connect(self.done_hdr10plus_extract)
+ self.hdr10plus_ffmpeg_signal.connect(lambda x: self.ffmpeg_level.setText(x))
+
+ def init_preset(self):
+ return self._add_combo_box(
+ label="Preset",
+ widget_name="preset",
+ options=presets,
+ tooltip="preset: The slower the preset, the better the compression and quality",
+ connect="default",
+ opt="preset",
+ )
+
+ def init_tune(self):
+ return self._add_combo_box(
+ label="Tune",
+ widget_name="tune",
+ tooltip="Tune the settings for a particular type of source or situation\nhq - High Quality, ll - Low Latency, ull - Ultra Low Latency",
+ options=["hq", "ll", "ull", "lossless"],
+ opt="tune",
+ )
+
+ # def init_profile(self):
+ # # TODO auto
+ # return self._add_combo_box(
+ # label="Profile_encoderopt",
+ # widget_name="profile",
+ # tooltip="Enforce an encode profile",
+ # options=["main", "main10"],
+ # opt="profile",
+ # )
+
+ # def init_tier(self):
+ # return self._add_combo_box(
+ # label="Tier",
+ # tooltip="Set the encoding tier",
+ # widget_name="tier",
+ # options=["main", "high"],
+ # opt="tier",
+ # )
+
+ def init_aq(self):
+ return self._add_combo_box(
+ label="Adaptive Quantization",
+ tooltip="",
+ widget_name="aq",
+ options=["off", "spatial", "temporal"],
+ opt="aq",
+ )
+
+ def init_aq_strength(self):
+ return self._add_combo_box(
+ label="AQ Strength",
+ tooltip="",
+ widget_name="aq_strength",
+ options=["Auto"] + [str(x) for x in range(1, 16)],
+ opt="aq_strength",
+ )
+
+ def init_multipass(self):
+ return self._add_combo_box(
+ label="Multipass",
+ tooltip="",
+ widget_name="multipass",
+ options=["None", "2pass-quarter", "2pass-full"],
+ opt="multipass",
+ )
+
+ def init_mv_precision(self):
+ return self._add_combo_box(
+ label="Motion vector accuracy",
+ tooltip="Q-pel is highest precision",
+ widget_name="mv_precision",
+ options=["Auto", "Q-pel", "half-pel", "full-pel"],
+ opt="mv_precision",
+ )
+
+ def init_lookahead(self):
+ return self._add_combo_box(
+ label="Lookahead",
+ tooltip="",
+ widget_name="lookahead",
+ opt="lookahead",
+ options=["off"] + [str(x) for x in range(1, 33)],
+ )
+
+ def init_level(self):
+ layout = self._add_combo_box(
+ label="Level",
+ tooltip="Set the encoding level restriction",
+ widget_name="level",
+ options=[
+ t("Auto"),
+ "1.0",
+ "2.0",
+ "2.1",
+ "3.0",
+ "3.1",
+ "4.0",
+ "4.1",
+ "5.0",
+ "5.1",
+ "5.2",
+ "6.0",
+ "6.1",
+ "6.2",
+ ],
+ opt="level",
+ )
+ self.widgets.level.setMinimumWidth(60)
+ return layout
+
+ def init_b_ref_mode(self):
+ layout = self._add_combo_box(
+ label="B Ref Mode",
+ tooltip="Use B frames as references",
+ widget_name="b_ref_mode",
+ opt="b_ref_mode",
+ options=["disabled", "each", "middle"],
+ min_width=60,
+ )
+ return layout
+
+ @staticmethod
+ def _qp_range():
+ return [str(x) for x in range(0, 52)]
+
+ def init_min_q(self):
+ layout = QtWidgets.QHBoxLayout()
+ layout.addWidget(QtWidgets.QLabel(t("Min Q")))
+ layout.addWidget(
+ self._add_combo_box(widget_name="min_q_i", options=["I"] + self._qp_range(), min_width=45, opt="min_q_i")
+ )
+ layout.addWidget(
+ self._add_combo_box(widget_name="min_q_p", options=["P"] + self._qp_range(), min_width=45, opt="min_q_p")
+ )
+ layout.addWidget(
+ self._add_combo_box(widget_name="min_q_b", options=["B"] + self._qp_range(), min_width=45, opt="min_q_b")
+ )
+ return layout
+
+ def init_init_q(self):
+ layout = QtWidgets.QHBoxLayout()
+ layout.addWidget(QtWidgets.QLabel(t("Init Q")))
+ layout.addWidget(
+ self._add_combo_box(widget_name="init_q_i", options=["I"] + self._qp_range(), min_width=45, opt="init_q_i")
+ )
+ layout.addWidget(
+ self._add_combo_box(widget_name="init_q_p", options=["P"] + self._qp_range(), min_width=45, opt="init_q_p")
+ )
+ layout.addWidget(
+ self._add_combo_box(widget_name="init_q_b", options=["B"] + self._qp_range(), min_width=45, opt="init_q_b")
+ )
+ return layout
+
+ def init_max_q(self):
+ layout = QtWidgets.QHBoxLayout()
+ layout.addWidget(QtWidgets.QLabel(t("Max Q")))
+ layout.addWidget(
+ self._add_combo_box(widget_name="max_q_i", options=["I"] + self._qp_range(), min_width=45, opt="max_q_i")
+ )
+ layout.addWidget(
+ self._add_combo_box(widget_name="max_q_p", options=["P"] + self._qp_range(), min_width=45, opt="max_q_p")
+ )
+ layout.addWidget(
+ self._add_combo_box(widget_name="max_q_b", options=["B"] + self._qp_range(), min_width=45, opt="max_q_b")
+ )
+ return layout
+
+ def init_vbr_target(self):
+ return self._add_combo_box(
+ widget_name="vbr_target",
+ label="VBR Target",
+ options=[t("Auto")] + self._qp_range(),
+ opt="vbr_target",
+ min_width=60,
+ )
+
+ def init_b_frames(self):
+ return self._add_combo_box(
+ widget_name="b_frames",
+ label="B Frames",
+ options=[t("Auto"), "0", "1", "2", "3", "4", "5", "6"],
+ opt="b_frames",
+ min_width=60,
+ )
+
+ def init_ref(self):
+ return self._add_combo_box(
+ widget_name="ref",
+ label="Ref Frames",
+ options=[t("Auto"), "0", "1", "2", "3", "4", "5", "6"],
+ opt="ref",
+ min_width=60,
+ )
+
+ def init_metrics(self):
+ return self._add_check_box(
+ widget_name="metrics",
+ opt="metrics",
+ label="Metrics",
+ tooltip="Calculate PSNR and SSIM and show in the encoder output",
+ )
+
+ def init_dhdr10_info(self):
+ layout = self._add_file_select(
+ label="HDR10+ Metadata",
+ widget_name="hdr10plus_metadata",
+ button_action=lambda: self.dhdr10_update(),
+ tooltip="dhdr10_info: Path to HDR10+ JSON metadata file",
+ )
+ self.labels["hdr10plus_metadata"].setFixedWidth(200)
+ self.extract_button = QtWidgets.QPushButton(t("Extract HDR10+"))
+ self.extract_button.hide()
+ self.extract_button.clicked.connect(self.extract_hdr10plus)
+
+ self.extract_label = QtWidgets.QLabel(self)
+ self.extract_label.hide()
+ self.movie = QtGui.QMovie(loading_movie)
+ self.movie.setScaledSize(QtCore.QSize(25, 25))
+ self.extract_label.setMovie(self.movie)
+
+ layout.addWidget(self.extract_button)
+ layout.addWidget(self.extract_label)
+
+ return layout
+
+ def init_modes(self):
+ layout = self._add_modes(recommended_bitrates, recommended_crfs, qp_name="cqp")
+ return layout
+
+ def mode_update(self):
+ self.widgets.custom_cqp.setDisabled(self.widgets.cqp.currentText() != "Custom")
+ self.widgets.custom_bitrate.setDisabled(self.widgets.bitrate.currentText() != "Custom")
+ self.main.build_commands()
+
+ def setting_change(self, update=True):
+ if self.updating_settings:
+ return
+ self.updating_settings = True
+
+ if update:
+ self.main.page_update()
+ self.updating_settings = False
+
+ def update_video_encoder_settings(self):
+ settings = NVEncCAVCSettings(
+ preset=self.widgets.preset.currentText().split("-")[0].strip(),
+ # profile=self.widgets.profile.currentText(),
+ # tier=self.widgets.tier.currentText(),
+ lookahead=self.widgets.lookahead.currentIndex() if self.widgets.lookahead.currentIndex() > 0 else None,
+ aq=self.widgets.aq.currentText(),
+ aq_strength=self.widgets.aq_strength.currentIndex(),
+ hdr10plus_metadata=self.widgets.hdr10plus_metadata.text().strip(), # .replace("\\", "/"),
+ multipass=self.widgets.multipass.currentText(),
+ mv_precision=self.widgets.mv_precision.currentText(),
+ init_q_i=self.widgets.init_q_i.currentText() if self.widgets.init_q_i.currentIndex() != 0 else None,
+ init_q_p=self.widgets.init_q_p.currentText() if self.widgets.init_q_p.currentIndex() != 0 else None,
+ init_q_b=self.widgets.init_q_b.currentText() if self.widgets.init_q_b.currentIndex() != 0 else None,
+ max_q_i=self.widgets.max_q_i.currentText() if self.widgets.max_q_i.currentIndex() != 0 else None,
+ max_q_p=self.widgets.max_q_p.currentText() if self.widgets.max_q_p.currentIndex() != 0 else None,
+ max_q_b=self.widgets.max_q_b.currentText() if self.widgets.max_q_b.currentIndex() != 0 else None,
+ min_q_i=self.widgets.min_q_i.currentText() if self.widgets.min_q_i.currentIndex() != 0 else None,
+ min_q_p=self.widgets.min_q_p.currentText() if self.widgets.min_q_p.currentIndex() != 0 else None,
+ min_q_b=self.widgets.min_q_b.currentText() if self.widgets.min_q_b.currentIndex() != 0 else None,
+ extra=self.ffmpeg_extras,
+ metrics=self.widgets.metrics.isChecked(),
+ level=self.widgets.level.currentText() if self.widgets.level.currentIndex() != 0 else None,
+ b_frames=self.widgets.b_frames.currentText() if self.widgets.b_frames.currentIndex() != 0 else None,
+ ref=self.widgets.ref.currentText() if self.widgets.ref.currentIndex() != 0 else None,
+ vbr_target=self.widgets.vbr_target.currentText() if self.widgets.vbr_target.currentIndex() > 0 else None,
+ b_ref_mode=self.widgets.b_ref_mode.currentText(),
+ )
+
+ encode_type, q_value = self.get_mode_settings()
+ settings.cqp = q_value if encode_type == "qp" else None
+ settings.bitrate = q_value if encode_type == "bitrate" else None
+ self.app.fastflix.current_video.video_settings.video_encoder_settings = settings
+
+ def set_mode(self, x):
+ self.mode = x.text()
+ for group in ("init", "max", "min"):
+ for frame_type in ("i", "p", "b"):
+ self.widgets[f"{group}_q_{frame_type}"].setEnabled(self.mode.lower() == "bitrate")
+ self.widgets.vbr_target.setEnabled(self.mode.lower() == "bitrate")
+ self.main.build_commands()
+
+ def new_source(self):
+ if not self.app.fastflix.current_video:
+ return
+ super().new_source()
+ if self.app.fastflix.current_video.hdr10_plus:
+ self.extract_button.show()
+ else:
+ self.extract_button.hide()
diff --git a/fastflix/encoders/nvencc_hevc/__init__.py b/fastflix/encoders/nvencc_hevc/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/fastflix/encoders/nvencc_hevc/command_builder.py b/fastflix/encoders/nvencc_hevc/command_builder.py
new file mode 100644
index 00000000..7b088468
--- /dev/null
+++ b/fastflix/encoders/nvencc_hevc/command_builder.py
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+import logging
+
+from fastflix.encoders.common.helpers import Command
+from fastflix.models.encode import NVEncCSettings
+from fastflix.models.video import Video
+from fastflix.models.fastflix import FastFlix
+from fastflix.encoders.common.nvencc_helpers import build_subtitle, build_audio
+from fastflix.flix import clean_file_string
+
+logger = logging.getLogger("fastflix")
+
+
+def build(fastflix: FastFlix):
+ video: Video = fastflix.current_video
+ settings: NVEncCSettings = fastflix.current_video.video_settings.video_encoder_settings
+
+ master_display = None
+ if fastflix.current_video.master_display:
+ master_display = (
+ f'--master-display "G{fastflix.current_video.master_display.green}'
+ f"B{fastflix.current_video.master_display.blue}"
+ f"R{fastflix.current_video.master_display.red}"
+ f"WP{fastflix.current_video.master_display.white}"
+ f'L{fastflix.current_video.master_display.luminance}"'
+ )
+
+ max_cll = None
+ if fastflix.current_video.cll:
+ max_cll = f'--max-cll "{fastflix.current_video.cll}"'
+
+ dhdr = None
+ if settings.hdr10plus_metadata:
+ dhdr = f'--dhdr10-info "{settings.hdr10plus_metadata}"'
+
+ trim = ""
+ try:
+ if "/" in video.frame_rate:
+ over, under = [int(x) for x in video.frame_rate.split("/")]
+ rate = over / under
+ else:
+ rate = float(video.frame_rate)
+ except Exception:
+ logger.exception("Could not get framerate of this movie!")
+ else:
+ if video.video_settings.end_time:
+ end_frame = int(video.video_settings.end_time * rate)
+ start_frame = 0
+ if video.video_settings.start_time:
+ start_frame = int(video.video_settings.start_time * rate)
+ trim = f"--trim {start_frame}:{end_frame}"
+ elif video.video_settings.start_time:
+ trim = f"--seek {video.video_settings.start_time}"
+
+ if (video.frame_rate != video.average_frame_rate) and trim:
+ logger.warning("Cannot use 'trim' when working with variable frame rate videos")
+ trim = ""
+
+ transform = ""
+ if video.video_settings.vertical_flip or video.video_settings.horizontal_flip:
+ transform = f"--vpp-transform flip_x={'true' if video.video_settings.horizontal_flip else 'false'},flip_y={'true' if video.video_settings.vertical_flip else 'false'}"
+
+ remove_hdr = ""
+ if video.video_settings.remove_hdr:
+ remove_type = (
+ video.video_settings.tone_map
+ if video.video_settings.tone_map in ("mobius", "hable", "reinhard")
+ else "mobius"
+ )
+ remove_hdr = f"--vpp-colorspace hdr2sdr={remove_type}" if video.video_settings.remove_hdr else ""
+
+ crop = ""
+ if video.video_settings.crop:
+ crop = f"--crop {video.video_settings.crop.left},{video.video_settings.crop.top},{video.video_settings.crop.right},{video.video_settings.crop.bottom}"
+
+ vbv = ""
+ if video.video_settings.maxrate:
+ vbv = f"--max-bitrate {video.video_settings.maxrate} --vbv-bufsize {video.video_settings.bufsize}"
+
+ init_q = settings.init_q_i
+ if settings.init_q_i and settings.init_q_p and settings.init_q_b:
+ init_q = f"{settings.init_q_i}:{settings.init_q_p}:{settings.init_q_b}"
+
+ min_q = settings.min_q_i
+ if settings.min_q_i and settings.min_q_p and settings.min_q_b:
+ min_q = f"{settings.min_q_i}:{settings.min_q_p}:{settings.min_q_b}"
+
+ max_q = settings.max_q_i
+ if settings.max_q_i and settings.max_q_p and settings.max_q_b:
+ max_q = f"{settings.max_q_i}:{settings.max_q_p}:{settings.max_q_b}"
+
+ try:
+ stream_id = int(video.current_video_stream["id"], 16)
+ except Exception:
+ if len(video.streams.video) > 1:
+ logger.warning("Could not get stream ID from source, the proper video track may not be selected!")
+ stream_id = None
+
+ aq = "--no-aq"
+ if settings.aq.lower() == "spatial":
+ aq = f"--aq --aq-strength {settings.aq_strength}"
+ elif settings.aq.lower() == "temporal":
+ aq = f"--aq-temporal --aq-strength {settings.aq_strength}"
+
+ command = [
+ f'"{clean_file_string(fastflix.config.nvencc)}"',
+ "-i",
+ f'"{clean_file_string(video.source)}"',
+ (f"--video-streamid {stream_id}" if stream_id else ""),
+ trim,
+ (f"--vpp-rotate {video.video_settings.rotate * 90}" if video.video_settings.rotate else ""),
+ transform,
+ (f'--output-res {video.video_settings.scale.replace(":", "x")}' if video.video_settings.scale else ""),
+ crop,
+ (f"--video-metadata clear" if video.video_settings.remove_metadata else "--video-metadata copy"),
+ (f'--video-metadata title="{video.video_settings.video_title}"' if video.video_settings.video_title else ""),
+ ("--chapter-copy" if video.video_settings.copy_chapters else ""),
+ "-c",
+ "hevc",
+ (f"--vbr {settings.bitrate.rstrip('k')}" if settings.bitrate else f"--cqp {settings.cqp}"),
+ vbv,
+ (f"--vbr-quality {settings.vbr_target}" if settings.vbr_target is not None and settings.bitrate else ""),
+ (f"--qp-init {init_q}" if init_q and settings.bitrate else ""),
+ (f"--qp-min {min_q}" if min_q and settings.bitrate else ""),
+ (f"--qp-max {max_q}" if max_q and settings.bitrate else ""),
+ (f"--bframes {settings.b_frames}" if settings.b_frames else ""),
+ (f"--ref {settings.ref}" if settings.ref else ""),
+ f"--bref-mode {settings.b_ref_mode}",
+ "--preset",
+ settings.preset,
+ "--tier",
+ settings.tier,
+ (f"--lookahead {settings.lookahead}" if settings.lookahead else ""),
+ aq,
+ "--colormatrix",
+ (video.video_settings.color_space or "auto"),
+ "--transfer",
+ (video.video_settings.color_transfer or "auto"),
+ "--colorprim",
+ (video.video_settings.color_primaries or "auto"),
+ (master_display if master_display else ""),
+ (max_cll if max_cll else ""),
+ (dhdr if dhdr else ""),
+ "--output-depth",
+ ("10" if video.current_video_stream.bit_depth > 8 and not video.video_settings.remove_hdr else "8"),
+ "--multipass",
+ settings.multipass,
+ "--mv-precision",
+ settings.mv_precision,
+ "--chromaloc",
+ "auto",
+ "--colorrange",
+ "auto",
+ f"--avsync {'cfr' if video.frame_rate == video.average_frame_rate else 'vfr'}",
+ (f"--interlace {video.interlaced}" if video.interlaced else ""),
+ ("--vpp-yadif" if video.video_settings.deinterlace else ""),
+ (f"--vpp-colorspace hdr2sdr=mobius" if video.video_settings.remove_hdr else ""),
+ remove_hdr,
+ "--psnr --ssim" if settings.metrics else "",
+ build_audio(video.video_settings.audio_tracks, video.streams.audio),
+ build_subtitle(video.video_settings.subtitle_tracks, video.streams.subtitle),
+ settings.extra,
+ "-o",
+ f'"{clean_file_string(video.video_settings.output_path)}"',
+ ]
+
+ return [Command(command=" ".join(x for x in command if x), name="NVEncC Encode", exe="NVEncE")]
diff --git a/fastflix/encoders/nvencc_hevc/main.py b/fastflix/encoders/nvencc_hevc/main.py
new file mode 100644
index 00000000..3bfabe9d
--- /dev/null
+++ b/fastflix/encoders/nvencc_hevc/main.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+__author__ = "Chris Griffith"
+from pathlib import Path
+
+import pkg_resources
+
+name = "HEVC (NVEncC)"
+
+video_extension = "mkv"
+video_dimension_divisor = 1
+icon = str(Path(pkg_resources.resource_filename(__name__, f"../../data/encoders/icon_nvencc.png")).resolve())
+
+enable_subtitles = True
+enable_audio = True
+enable_attachments = False
+
+audio_formats = [
+ "aac",
+ "aac_mf",
+ "libfdk_aac",
+ "ac3",
+ "ac3_fixed",
+ "ac3_mf",
+ "adpcm_adx",
+ "g722",
+ "g726",
+ "g726le",
+ "adpcm_ima_qt",
+ "adpcm_ima_ssi",
+ "adpcm_ima_wav",
+ "adpcm_ms",
+ "adpcm_swf",
+ "adpcm_yamaha",
+ "alac",
+ "libopencore_amrnb",
+ "libvo_amrwbenc",
+ "aptx",
+ "aptx_hd",
+ "comfortnoise",
+ "dca",
+ "eac3",
+ "flac",
+ "g723_1",
+ "libgsm",
+ "libgsm_ms",
+ "libilbc",
+ "mlp",
+ "mp2",
+ "mp2fixed",
+ "libtwolame",
+ "mp3_mf",
+ "libmp3lame",
+ "nellymoser",
+ "opus",
+ "libopus",
+ "pcm_alaw",
+ "pcm_dvd",
+ "pcm_f32be",
+ "pcm_f32le",
+ "pcm_f64be",
+ "pcm_f64le",
+ "pcm_mulaw",
+ "pcm_s16be",
+ "pcm_s16be_planar",
+ "pcm_s16le",
+ "pcm_s16le_planar",
+ "pcm_s24be",
+ "pcm_s24daud",
+ "pcm_s24le",
+ "pcm_s24le_planar",
+ "pcm_s32be",
+ "pcm_s32le",
+ "pcm_s32le_planar",
+ "pcm_s64be",
+ "pcm_s64le",
+ "pcm_s8",
+ "pcm_s8_planar",
+ "pcm_u16be",
+ "pcm_u16le",
+ "pcm_u24be",
+ "pcm_u24le",
+ "pcm_u32be",
+ "pcm_u32le",
+ "pcm_u8",
+ "pcm_vidc",
+ "real_144",
+ "roq_dpcm",
+ "s302m",
+ "sbc",
+ "sonic",
+ "sonicls",
+ "libspeex",
+ "truehd",
+ "tta",
+ "vorbis",
+ "libvorbis",
+ "wavpack",
+ "wmav1",
+ "wmav2",
+]
+
+from fastflix.encoders.nvencc_hevc.command_builder import build
+from fastflix.encoders.nvencc_hevc.settings_panel import NVENCC as settings_panel
diff --git a/fastflix/encoders/nvencc_hevc/settings_panel.py b/fastflix/encoders/nvencc_hevc/settings_panel.py
new file mode 100644
index 00000000..19dc151c
--- /dev/null
+++ b/fastflix/encoders/nvencc_hevc/settings_panel.py
@@ -0,0 +1,441 @@
+# -*- coding: utf-8 -*-
+import logging
+from typing import List, Optional
+
+from box import Box
+from qtpy import QtCore, QtWidgets, QtGui
+
+from fastflix.encoders.common.setting_panel import SettingPanel
+from fastflix.language import t
+from fastflix.models.encode import NVEncCSettings
+from fastflix.models.fastflix_app import FastFlixApp
+from fastflix.shared import link
+from fastflix.exceptions import FastFlixInternalException
+from fastflix.resources import loading_movie, warning_icon
+
+logger = logging.getLogger("fastflix")
+
+presets = ["default", "performance", "quality"]
+
+recommended_bitrates = [
+ "200k (320x240p @ 30fps)",
+ "300k (640x360p @ 30fps)",
+ "1000k (640x480p @ 30fps)",
+ "1750k (1280x720p @ 30fps)",
+ "2500k (1280x720p @ 60fps)",
+ "4000k (1920x1080p @ 30fps)",
+ "5000k (1920x1080p @ 60fps)",
+ "7000k (2560x1440p @ 30fps)",
+ "10000k (2560x1440p @ 60fps)",
+ "15000k (3840x2160p @ 30fps)",
+ "20000k (3840x2160p @ 60fps)",
+ "Custom",
+]
+
+recommended_crfs = [
+ "28",
+ "27",
+ "26",
+ "25",
+ "24",
+ "23",
+ "22",
+ "21",
+ "20",
+ "19",
+ "18",
+ "17",
+ "16",
+ "15",
+ "14",
+ "Custom",
+]
+
+
+def get_breaker():
+ breaker_line = QtWidgets.QWidget()
+ breaker_line.setMaximumHeight(2)
+ breaker_line.setStyleSheet("background-color: #ccc; margin: auto 0; padding: auto 0;")
+ return breaker_line
+
+
+class NVENCC(SettingPanel):
+ profile_name = "nvencc_hevc"
+ hdr10plus_signal = QtCore.Signal(str)
+ hdr10plus_ffmpeg_signal = QtCore.Signal(str)
+
+ def __init__(self, parent, main, app: FastFlixApp):
+ super().__init__(parent, main, app)
+ self.main = main
+ self.app = app
+
+ grid = QtWidgets.QGridLayout()
+
+ self.widgets = Box(mode=None)
+
+ self.mode = "Bitrate"
+ self.updating_settings = False
+
+ grid.addLayout(self.init_modes(), 0, 2, 4, 4)
+ grid.addLayout(self._add_custom(title="Custom NVEncC options", disable_both_passes=True), 10, 0, 1, 6)
+
+ grid.addLayout(self.init_preset(), 0, 0, 1, 2)
+ # grid.addLayout(self.init_profile(), 1, 0, 1, 2)
+ grid.addLayout(self.init_tier(), 1, 0, 1, 2)
+ grid.addLayout(self.init_multipass(), 2, 0, 1, 2)
+ grid.addLayout(self.init_lookahead(), 3, 0, 1, 2)
+
+ breaker = QtWidgets.QHBoxLayout()
+ breaker_label = QtWidgets.QLabel(t("Advanced"))
+ breaker_label.setFont(QtGui.QFont("helvetica", 8, weight=55))
+
+ breaker.addWidget(get_breaker(), stretch=1)
+ breaker.addWidget(breaker_label, alignment=QtCore.Qt.AlignHCenter)
+ breaker.addWidget(get_breaker(), stretch=1)
+
+ grid.addLayout(breaker, 4, 0, 1, 6)
+
+ grid.addLayout(self.init_aq(), 5, 0, 1, 2)
+ grid.addLayout(self.init_aq_strength(), 6, 0, 1, 2)
+ grid.addLayout(self.init_mv_precision(), 7, 0, 1, 2)
+
+ qp_line = QtWidgets.QHBoxLayout()
+ qp_line.addLayout(self.init_vbr_target())
+ qp_line.addStretch(1)
+ qp_line.addLayout(self.init_init_q())
+ qp_line.addStretch(1)
+ qp_line.addLayout(self.init_min_q())
+ qp_line.addStretch(1)
+ qp_line.addLayout(self.init_max_q())
+
+ grid.addLayout(qp_line, 5, 2, 1, 4)
+
+ advanced = QtWidgets.QHBoxLayout()
+ advanced.addLayout(self.init_ref())
+ advanced.addStretch(1)
+ advanced.addLayout(self.init_b_frames())
+ advanced.addStretch(1)
+ advanced.addLayout(self.init_level())
+ advanced.addStretch(1)
+ advanced.addLayout(self.init_b_ref_mode())
+ advanced.addStretch(1)
+ advanced.addLayout(self.init_metrics())
+ grid.addLayout(advanced, 6, 2, 1, 4)
+
+ grid.addLayout(self.init_dhdr10_info(), 7, 2, 1, 4)
+
+ self.ffmpeg_level = QtWidgets.QLabel()
+ grid.addWidget(self.ffmpeg_level, 8, 2, 1, 4)
+
+ grid.setRowStretch(9, 1)
+
+ guide_label = QtWidgets.QLabel(
+ link("https://github.com/rigaya/NVEnc/blob/master/NVEncC_Options.en.md", t("NVEncC Options"))
+ )
+
+ warning_label = QtWidgets.QLabel()
+ warning_label.setPixmap(QtGui.QIcon(warning_icon).pixmap(22))
+
+ guide_label.setAlignment(QtCore.Qt.AlignBottom)
+ guide_label.setOpenExternalLinks(True)
+ grid.addWidget(guide_label, 11, 0, 1, 4)
+ grid.addWidget(warning_label, 11, 4, 1, 1, alignment=QtCore.Qt.AlignRight)
+ grid.addWidget(QtWidgets.QLabel(t("NVEncC Encoder support is still experimental!")), 11, 5, 1, 1)
+
+ self.setLayout(grid)
+ self.hide()
+ self.hdr10plus_signal.connect(self.done_hdr10plus_extract)
+ self.hdr10plus_ffmpeg_signal.connect(lambda x: self.ffmpeg_level.setText(x))
+
+ def init_preset(self):
+ return self._add_combo_box(
+ label="Preset",
+ widget_name="preset",
+ options=presets,
+ tooltip="preset: The slower the preset, the better the compression and quality",
+ connect="default",
+ opt="preset",
+ )
+
+ def init_tune(self):
+ return self._add_combo_box(
+ label="Tune",
+ widget_name="tune",
+ tooltip="Tune the settings for a particular type of source or situation\nhq - High Quality, ll - Low Latency, ull - Ultra Low Latency",
+ options=["hq", "ll", "ull", "lossless"],
+ opt="tune",
+ )
+
+ # def init_profile(self):
+ # # TODO auto
+ # return self._add_combo_box(
+ # label="Profile_encoderopt",
+ # widget_name="profile",
+ # tooltip="Enforce an encode profile",
+ # options=["main", "main10"],
+ # opt="profile",
+ # )
+
+ def init_tier(self):
+ return self._add_combo_box(
+ label="Tier",
+ tooltip="Set the encoding tier",
+ widget_name="tier",
+ options=["main", "high"],
+ opt="tier",
+ )
+
+ def init_aq(self):
+ return self._add_combo_box(
+ label="Adaptive Quantization",
+ tooltip="",
+ widget_name="aq",
+ options=["off", "spatial", "temporal"],
+ opt="aq",
+ )
+
+ def init_aq_strength(self):
+ return self._add_combo_box(
+ label="AQ Strength",
+ tooltip="",
+ widget_name="aq_strength",
+ options=["Auto"] + [str(x) for x in range(1, 16)],
+ opt="aq_strength",
+ )
+
+ def init_multipass(self):
+ return self._add_combo_box(
+ label="Multipass",
+ tooltip="",
+ widget_name="multipass",
+ options=["None", "2pass-quarter", "2pass-full"],
+ opt="multipass",
+ )
+
+ def init_mv_precision(self):
+ return self._add_combo_box(
+ label="Motion vector accuracy",
+ tooltip="Q-pel is highest precision",
+ widget_name="mv_precision",
+ options=["Auto", "Q-pel", "half-pel", "full-pel"],
+ opt="mv_precision",
+ )
+
+ def init_lookahead(self):
+ return self._add_combo_box(
+ label="Lookahead",
+ tooltip="",
+ widget_name="lookahead",
+ opt="lookahead",
+ options=["off"] + [str(x) for x in range(1, 33)],
+ )
+
+ def init_level(self):
+ layout = self._add_combo_box(
+ label="Level",
+ tooltip="Set the encoding level restriction",
+ widget_name="level",
+ options=[
+ t("Auto"),
+ "1.0",
+ "2.0",
+ "2.1",
+ "3.0",
+ "3.1",
+ "4.0",
+ "4.1",
+ "5.0",
+ "5.1",
+ "5.2",
+ "6.0",
+ "6.1",
+ "6.2",
+ ],
+ opt="level",
+ )
+ self.widgets.level.setMinimumWidth(60)
+ return layout
+
+ def init_b_ref_mode(self):
+ layout = self._add_combo_box(
+ label="B Ref Mode",
+ tooltip="Use B frames as references",
+ widget_name="b_ref_mode",
+ opt="b_ref_mode",
+ options=["disabled", "each", "middle"],
+ min_width=60,
+ )
+ return layout
+
+ @staticmethod
+ def _qp_range():
+ return [str(x) for x in range(0, 52)]
+
+ def init_min_q(self):
+ layout = QtWidgets.QHBoxLayout()
+ layout.addWidget(QtWidgets.QLabel(t("Min Q")))
+ layout.addWidget(
+ self._add_combo_box(widget_name="min_q_i", options=["I"] + self._qp_range(), min_width=45, opt="min_q_i")
+ )
+ layout.addWidget(
+ self._add_combo_box(widget_name="min_q_p", options=["P"] + self._qp_range(), min_width=45, opt="min_q_p")
+ )
+ layout.addWidget(
+ self._add_combo_box(widget_name="min_q_b", options=["B"] + self._qp_range(), min_width=45, opt="min_q_b")
+ )
+ return layout
+
+ def init_init_q(self):
+ layout = QtWidgets.QHBoxLayout()
+ layout.addWidget(QtWidgets.QLabel(t("Init Q")))
+ layout.addWidget(
+ self._add_combo_box(widget_name="init_q_i", options=["I"] + self._qp_range(), min_width=45, opt="init_q_i")
+ )
+ layout.addWidget(
+ self._add_combo_box(widget_name="init_q_p", options=["P"] + self._qp_range(), min_width=45, opt="init_q_p")
+ )
+ layout.addWidget(
+ self._add_combo_box(widget_name="init_q_b", options=["B"] + self._qp_range(), min_width=45, opt="init_q_b")
+ )
+ return layout
+
+ def init_max_q(self):
+ layout = QtWidgets.QHBoxLayout()
+ layout.addWidget(QtWidgets.QLabel(t("Max Q")))
+ layout.addWidget(
+ self._add_combo_box(widget_name="max_q_i", options=["I"] + self._qp_range(), min_width=45, opt="max_q_i")
+ )
+ layout.addWidget(
+ self._add_combo_box(widget_name="max_q_p", options=["P"] + self._qp_range(), min_width=45, opt="max_q_p")
+ )
+ layout.addWidget(
+ self._add_combo_box(widget_name="max_q_b", options=["B"] + self._qp_range(), min_width=45, opt="max_q_b")
+ )
+ return layout
+
+ def init_vbr_target(self):
+ return self._add_combo_box(
+ widget_name="vbr_target",
+ label="VBR Target",
+ options=[t("Auto")] + self._qp_range(),
+ opt="vbr_target",
+ min_width=60,
+ )
+
+ def init_b_frames(self):
+ return self._add_combo_box(
+ widget_name="b_frames",
+ label="B Frames",
+ options=[t("Auto"), "0", "1", "2", "3", "4", "5", "6"],
+ opt="b_frames",
+ min_width=60,
+ )
+
+ def init_ref(self):
+ return self._add_combo_box(
+ widget_name="ref",
+ label="Ref Frames",
+ options=[t("Auto"), "0", "1", "2", "3", "4", "5", "6"],
+ opt="ref",
+ min_width=60,
+ )
+
+ def init_metrics(self):
+ return self._add_check_box(
+ widget_name="metrics",
+ opt="metrics",
+ label="Metrics",
+ tooltip="Calculate PSNR and SSIM and show in the encoder output",
+ )
+
+ def init_dhdr10_info(self):
+ layout = self._add_file_select(
+ label="HDR10+ Metadata",
+ widget_name="hdr10plus_metadata",
+ button_action=lambda: self.dhdr10_update(),
+ tooltip="dhdr10_info: Path to HDR10+ JSON metadata file",
+ )
+ self.labels["hdr10plus_metadata"].setFixedWidth(200)
+ self.extract_button = QtWidgets.QPushButton(t("Extract HDR10+"))
+ self.extract_button.hide()
+ self.extract_button.clicked.connect(self.extract_hdr10plus)
+
+ self.extract_label = QtWidgets.QLabel(self)
+ self.extract_label.hide()
+ self.movie = QtGui.QMovie(loading_movie)
+ self.movie.setScaledSize(QtCore.QSize(25, 25))
+ self.extract_label.setMovie(self.movie)
+
+ layout.addWidget(self.extract_button)
+ layout.addWidget(self.extract_label)
+
+ return layout
+
+ def init_modes(self):
+ layout = self._add_modes(recommended_bitrates, recommended_crfs, qp_name="cqp")
+ return layout
+
+ def mode_update(self):
+ self.widgets.custom_cqp.setDisabled(self.widgets.cqp.currentText() != "Custom")
+ self.widgets.custom_bitrate.setDisabled(self.widgets.bitrate.currentText() != "Custom")
+ self.main.build_commands()
+
+ def setting_change(self, update=True):
+ if self.updating_settings:
+ return
+ self.updating_settings = True
+
+ if update:
+ self.main.page_update()
+ self.updating_settings = False
+
+ def update_video_encoder_settings(self):
+ settings = NVEncCSettings(
+ preset=self.widgets.preset.currentText().split("-")[0].strip(),
+ # profile=self.widgets.profile.currentText(),
+ tier=self.widgets.tier.currentText(),
+ lookahead=self.widgets.lookahead.currentIndex() if self.widgets.lookahead.currentIndex() > 0 else None,
+ aq=self.widgets.aq.currentText(),
+ aq_strength=self.widgets.aq_strength.currentIndex(),
+ hdr10plus_metadata=self.widgets.hdr10plus_metadata.text().strip(), # .replace("\\", "/"),
+ multipass=self.widgets.multipass.currentText(),
+ mv_precision=self.widgets.mv_precision.currentText(),
+ init_q_i=self.widgets.init_q_i.currentText() if self.widgets.init_q_i.currentIndex() != 0 else None,
+ init_q_p=self.widgets.init_q_p.currentText() if self.widgets.init_q_p.currentIndex() != 0 else None,
+ init_q_b=self.widgets.init_q_b.currentText() if self.widgets.init_q_b.currentIndex() != 0 else None,
+ max_q_i=self.widgets.max_q_i.currentText() if self.widgets.max_q_i.currentIndex() != 0 else None,
+ max_q_p=self.widgets.max_q_p.currentText() if self.widgets.max_q_p.currentIndex() != 0 else None,
+ max_q_b=self.widgets.max_q_b.currentText() if self.widgets.max_q_b.currentIndex() != 0 else None,
+ min_q_i=self.widgets.min_q_i.currentText() if self.widgets.min_q_i.currentIndex() != 0 else None,
+ min_q_p=self.widgets.min_q_p.currentText() if self.widgets.min_q_p.currentIndex() != 0 else None,
+ min_q_b=self.widgets.min_q_b.currentText() if self.widgets.min_q_b.currentIndex() != 0 else None,
+ extra=self.ffmpeg_extras,
+ metrics=self.widgets.metrics.isChecked(),
+ level=self.widgets.level.currentText() if self.widgets.level.currentIndex() != 0 else None,
+ b_frames=self.widgets.b_frames.currentText() if self.widgets.b_frames.currentIndex() != 0 else None,
+ ref=self.widgets.ref.currentText() if self.widgets.ref.currentIndex() != 0 else None,
+ vbr_target=self.widgets.vbr_target.currentText() if self.widgets.vbr_target.currentIndex() > 0 else None,
+ b_ref_mode=self.widgets.b_ref_mode.currentText(),
+ )
+
+ encode_type, q_value = self.get_mode_settings()
+ settings.cqp = q_value if encode_type == "qp" else None
+ settings.bitrate = q_value if encode_type == "bitrate" else None
+ self.app.fastflix.current_video.video_settings.video_encoder_settings = settings
+
+ def set_mode(self, x):
+ self.mode = x.text()
+ for group in ("init", "max", "min"):
+ for frame_type in ("i", "p", "b"):
+ self.widgets[f"{group}_q_{frame_type}"].setEnabled(self.mode.lower() == "bitrate")
+ self.widgets.vbr_target.setEnabled(self.mode.lower() == "bitrate")
+ self.main.build_commands()
+
+ def new_source(self):
+ if not self.app.fastflix.current_video:
+ return
+ super().new_source()
+ if self.app.fastflix.current_video.hdr10_plus:
+ self.extract_button.show()
+ else:
+ self.extract_button.hide()
diff --git a/fastflix/encoders/rav1e/command_builder.py b/fastflix/encoders/rav1e/command_builder.py
index 6d6d62aa..2d414163 100644
--- a/fastflix/encoders/rav1e/command_builder.py
+++ b/fastflix/encoders/rav1e/command_builder.py
@@ -45,8 +45,6 @@ def build(fastflix: FastFlix):
# opts = ":".join(rav1e_options)
# beginning += f'-rav1e-params "{opts}"'
- beginning = re.sub("[ ]+", " ", beginning)
-
if not settings.single_pass:
pass_log_file = fastflix.current_video.work_path / f"pass_log_file_{secrets.token_hex(10)}"
beginning += f'-passlogfile "{pass_log_file}" '
diff --git a/fastflix/encoders/rav1e/settings_panel.py b/fastflix/encoders/rav1e/settings_panel.py
index 6b74e8e7..73867b60 100644
--- a/fastflix/encoders/rav1e/settings_panel.py
+++ b/fastflix/encoders/rav1e/settings_panel.py
@@ -106,10 +106,12 @@ def init_tile_columns(self):
)
def init_tiles(self):
- return self._add_combo_box("Tiles", [str(x) for x in range(-1, 17)], "tiles", opt="tiles")
+ return self._add_combo_box(
+ label="Tiles", options=[str(x) for x in range(-1, 17)], widget_name="tiles", opt="tiles"
+ )
def init_single_pass(self):
- return self._add_check_box("Single Pass (Bitrate)", "single_pass", opt="single_pass")
+ return self._add_check_box(label="Single Pass (Bitrate)", widget_name="single_pass", opt="single_pass")
def init_pix_fmt(self):
return self._add_combo_box(
diff --git a/fastflix/encoders/svt_av1/command_builder.py b/fastflix/encoders/svt_av1/command_builder.py
index de7e14e4..f8d97272 100644
--- a/fastflix/encoders/svt_av1/command_builder.py
+++ b/fastflix/encoders/svt_av1/command_builder.py
@@ -28,8 +28,6 @@ def build(fastflix: FastFlix):
f"{generate_color_details(fastflix)} "
)
- beginning = re.sub("[ ]+", " ", beginning)
-
if not settings.single_pass:
pass_log_file = fastflix.current_video.work_path / f"pass_log_file_{secrets.token_hex(10)}"
beginning += f'-passlogfile "{pass_log_file}" '
diff --git a/fastflix/encoders/vp9/command_builder.py b/fastflix/encoders/vp9/command_builder.py
index 5f6783cd..1ebe86af 100644
--- a/fastflix/encoders/vp9/command_builder.py
+++ b/fastflix/encoders/vp9/command_builder.py
@@ -22,18 +22,18 @@ def build(fastflix: FastFlix):
# if fastflix.current_video.color_space.startswith("bt2020"):
# beginning += "-color_primaries bt2020 -color_trc smpte2084 -colorspace bt2020nc -color_range 1"
- beginning = re.sub("[ ]+", " ", beginning)
-
- details = f"-quality {settings.quality} -speed {settings.speed} -profile:v {settings.profile}"
+ details = f"-quality:v {settings.quality} -profile:v {settings.profile} -tile-columns:v {settings.tile_columns} -tile-rows:v {settings.tile_rows} "
if settings.bitrate:
- command_1 = f"{beginning} -b:v {settings.bitrate} {details} -pass 1 {settings.extra if settings.extra_both_passes else ''} -an -f webm {null}"
- command_2 = f"{beginning} -b:v {settings.bitrate} {details} -pass 2 {settings.extra} {ending}"
+ command_1 = f"{beginning} -speed:v {'4' if settings.fast_first_pass else settings.speed} -b:v {settings.bitrate} {details} -pass 1 {settings.extra if settings.extra_both_passes else ''} -an -f webm {null}"
+ command_2 = (
+ f"{beginning} -speed:v {settings.speed} -b:v {settings.bitrate} {details} -pass 2 {settings.extra} {ending}"
+ )
elif settings.crf:
- command_1 = f"{beginning} -b:v 0 -crf {settings.crf} {details} -pass 1 {settings.extra if settings.extra_both_passes else ''} -an -f webm {null}"
+ command_1 = f"{beginning} -b:v 0 -crf:v {settings.crf} {details} -pass 1 {settings.extra if settings.extra_both_passes else ''} -an -f webm {null}"
command_2 = (
- f"{beginning} -b:v 0 -crf {settings.crf} {details} "
+ f"{beginning} -b:v 0 -crf:v {settings.crf} {details} "
f'{"-pass 2" if not settings.single_pass else ""} {settings.extra} {ending}'
)
diff --git a/fastflix/encoders/vp9/main.py b/fastflix/encoders/vp9/main.py
index 62459333..dc1ad009 100644
--- a/fastflix/encoders/vp9/main.py
+++ b/fastflix/encoders/vp9/main.py
@@ -10,8 +10,8 @@
icon = str(Path(pkg_resources.resource_filename(__name__, f"../../data/encoders/icon_vp9.png")).resolve())
-video_extension = "webm"
-video_dimension_divisor = 1
+video_extension = "mkv"
+video_dimension_divisor = 2
enable_subtitles = False
enable_audio = True
diff --git a/fastflix/encoders/vp9/settings_panel.py b/fastflix/encoders/vp9/settings_panel.py
index 6c426468..8f72e494 100644
--- a/fastflix/encoders/vp9/settings_panel.py
+++ b/fastflix/encoders/vp9/settings_panel.py
@@ -61,9 +61,19 @@ def __init__(self, parent, main, app: FastFlixApp):
grid.addLayout(self.init_max_mux(), 3, 0, 1, 2)
grid.addLayout(self.init_profile(), 4, 0, 1, 2)
+ grid.addLayout(self.init_tile_columns(), 5, 0, 1, 2)
+ grid.addLayout(self.init_tile_rows(), 6, 0, 1, 2)
+
grid.addLayout(self.init_modes(), 0, 2, 5, 4)
- grid.addLayout(self.init_single_pass(), 5, 2, 1, 2)
- grid.addLayout(self.init_row_mt(), 5, 4, 1, 2)
+
+ checkboxes = QtWidgets.QHBoxLayout()
+ checkboxes.addLayout(self.init_single_pass())
+ checkboxes.addStretch(1)
+ checkboxes.addLayout(self.init_row_mt())
+ checkboxes.addStretch(1)
+ checkboxes.addLayout(self.init_fast_first_pass())
+
+ grid.addLayout(checkboxes, 5, 2, 1, 4)
# grid.addWidget(QtWidgets.QWidget(), 8, 0)
grid.setRowStretch(8, 1)
@@ -138,6 +148,32 @@ def init_row_mt(self):
opt="row_mt",
)
+ def init_tile_columns(self):
+ return self._add_combo_box(
+ label="Tile Columns",
+ tooltip="Log2 of number of tile columns to encode faster (lesser quality)",
+ widget_name="tile_columns",
+ options=[str(x) for x in range(-1, 7)],
+ opt="tile_columns",
+ )
+
+ def init_tile_rows(self):
+ return self._add_combo_box(
+ label="Tile Rows",
+ tooltip="Log2 of number of tile rows to encode faster (lesser quality)",
+ widget_name="tile_rows",
+ options=[str(x) for x in range(-1, 3)],
+ opt="tile_rows",
+ )
+
+ def init_fast_first_pass(self):
+ return self._add_check_box(
+ label="Fast first pass",
+ tooltip="Set speed to 4 for first pass",
+ widget_name="fast_first_pass",
+ opt="fast_first_pass",
+ )
+
def init_single_pass(self):
return self._add_check_box(label="Single Pass (CRF)", tooltip="", widget_name="single_pass", opt="single_pass")
@@ -160,6 +196,11 @@ def update_video_encoder_settings(self):
profile=self.widgets.profile.currentIndex(),
extra=self.ffmpeg_extras,
extra_both_passes=self.widgets.extra_both_passes.isChecked(),
+ fast_first_pass=self.widgets.fast_first_pass.isChecked(),
+ tile_columns=self.widgets.tile_columns.currentText()
+ if self.widgets.tile_columns.currentIndex() > 0
+ else "-1",
+ tile_rows=self.widgets.tile_rows.currentText() if self.widgets.tile_rows.currentIndex() > 0 else "-1",
)
encode_type, q_value = self.get_mode_settings()
settings.crf = q_value if encode_type == "qp" else None
diff --git a/fastflix/encoders/webp/settings_panel.py b/fastflix/encoders/webp/settings_panel.py
index 1945962e..e2296b6c 100644
--- a/fastflix/encoders/webp/settings_panel.py
+++ b/fastflix/encoders/webp/settings_panel.py
@@ -31,20 +31,23 @@ def __init__(self, parent, main, app: FastFlixApp):
self.setLayout(grid)
def init_lossless(self):
- return self._add_combo_box("lossless", ["yes", "no"], "lossless", default=1)
+ return self._add_combo_box(label="lossless", options=["yes", "no"], widget_name="lossless", default=1)
def init_compression(self):
return self._add_combo_box(
- "compression level",
- ["0", "1", "2", "3", "4", "5", "6"],
- "compression",
+ label="compression level",
+ options=["0", "1", "2", "3", "4", "5", "6"],
+ widget_name="compression",
tooltip="For lossy, this is a quality/speed tradeoff.\nFor lossless, this is a size/speed tradeoff.",
default=4,
)
def init_preset(self):
return self._add_combo_box(
- "preset", ["none", "default", "picture", "photo", "drawing", "icon", "text"], "preset", default=1
+ label="preset",
+ options=["none", "default", "picture", "photo", "drawing", "icon", "text"],
+ widget_name="preset",
+ default=1,
)
def init_modes(self):
diff --git a/fastflix/entry.py b/fastflix/entry.py
index c1aeda9a..881284e7 100644
--- a/fastflix/entry.py
+++ b/fastflix/entry.py
@@ -2,7 +2,7 @@
import logging
import sys
import traceback
-from multiprocessing import Process, Queue, freeze_support
+from multiprocessing import Process, Queue, freeze_support, Manager, Lock
try:
import coloredlogs
@@ -26,12 +26,12 @@
sys.exit(1)
-def separate_app_process(worker_queue, status_queue, log_queue):
+def separate_app_process(worker_queue, status_queue, log_queue, queue_list, queue_lock):
""" This prevents any QT components being imported in the main process"""
from fastflix.application import start_app
freeze_support()
- start_app(worker_queue, status_queue, log_queue)
+ start_app(worker_queue, status_queue, log_queue, queue_list, queue_lock)
def startup_options():
@@ -66,7 +66,6 @@ def startup_options():
import fastflix.encoders.webp.main
import fastflix.flix
import fastflix.language
- import fastflix.models.base
import fastflix.models.config
import fastflix.models.encode
import fastflix.models.fastflix
@@ -117,19 +116,18 @@ def main():
status_queue = Queue()
log_queue = Queue()
- gui_proc = Process(
- target=separate_app_process,
- args=(
- worker_queue,
- status_queue,
- log_queue,
- ),
- )
- gui_proc.start()
- exit_status = 1
- try:
- queue_worker(gui_proc, worker_queue, status_queue, log_queue)
- exit_status = 0
- finally:
- gui_proc.kill()
- return exit_status
+ queue_lock = Lock()
+ with Manager() as manager:
+ queue_list = manager.list()
+ gui_proc = Process(
+ target=separate_app_process,
+ args=(worker_queue, status_queue, log_queue, queue_list, queue_lock),
+ )
+ gui_proc.start()
+ exit_status = 1
+ try:
+ queue_worker(gui_proc, worker_queue, status_queue, log_queue, queue_list, queue_lock)
+ exit_status = 0
+ finally:
+ gui_proc.kill()
+ return exit_status
diff --git a/fastflix/flix.py b/fastflix/flix.py
index a7a7de90..a1ce47ad 100644
--- a/fastflix/flix.py
+++ b/fastflix/flix.py
@@ -3,11 +3,12 @@
import os
import re
from pathlib import Path
-from subprocess import PIPE, CompletedProcess, TimeoutExpired, run
+from subprocess import PIPE, CompletedProcess, Popen, TimeoutExpired, run
from typing import List, Tuple, Union
import reusables
from box import Box, BoxError
+from pathvalidate import sanitize_filepath
from fastflix.exceptions import FlixError
from fastflix.language import t
@@ -22,8 +23,8 @@
logger = logging.getLogger("fastflix")
-def unixy(source):
- return str(source).replace("\\", "/")
+def clean_file_string(source):
+ return str(source).strip()
def guess_bit_depth(pix_fmt: str, color_primaries: str = None) -> int:
@@ -71,8 +72,7 @@ def guess_bit_depth(pix_fmt: str, color_primaries: str = None) -> int:
if color_primaries and color_primaries.startswith("bt2020"):
return 10
- else:
- return 8
+ return 8
def execute(command: List, work_dir: Union[Path, str] = None, timeout: int = None) -> CompletedProcess:
@@ -131,7 +131,7 @@ def probe(app: FastFlixApp, file: Path) -> Box:
"json",
"-show_format",
"-show_streams",
- f"{unixy(file)}",
+ f"{clean_file_string(file)}",
]
result = execute(command)
if result.returncode != 0:
@@ -147,30 +147,16 @@ def probe(app: FastFlixApp, file: Path) -> Box:
raise FlixError(result.stderr)
-def determine_rotation(streams) -> Tuple[int, int]:
- rotation = 0
- if "rotate" in streams.video[0].get("tags", {}):
- rotation = abs(int(streams.video[0].tags.rotate))
- # elif 'side_data_list' in self.streams.video[0]:
- # rots = [abs(int(x.rotation)) for x in self.streams.video[0].side_data_list if 'rotation' in x]
- # rotation = rots[0] if rots else 0
-
- if rotation in (90, 270):
- video_width = streams.video[0].height
- video_height = streams.video[0].width
- else:
- video_width = streams.video[0].width
- video_height = streams.video[0].height
- return video_width, video_height
-
-
def parse(app: FastFlixApp, **_):
data = probe(app, app.fastflix.current_video.source)
if "streams" not in data:
raise FlixError(f"Not a video file, FFprobe output: {data}")
streams = Box({"video": [], "audio": [], "subtitle": [], "attachment": [], "data": []})
for track in data.streams:
- if track.codec_type == "video" and track.get("disposition", {}).get("attached_pic"):
+ if track.codec_type == "video" and (
+ track.get("disposition", {}).get("attached_pic")
+ or track.get("tags", {}).get("MIMETYPE", "").startswith("image")
+ ):
streams.attachment.append(track)
elif track.codec_type in streams:
streams[track.codec_type].append(track)
@@ -188,7 +174,6 @@ def parse(app: FastFlixApp, **_):
app.fastflix.current_video.streams = streams
app.fastflix.current_video.video_settings.selected_track = streams.video[0].index
- app.fastflix.current_video.width, app.fastflix.current_video.height = determine_rotation(streams)
app.fastflix.current_video.format = data.format
app.fastflix.current_video.duration = float(data.format.get("duration", 0))
@@ -213,14 +198,14 @@ def extract_attachment(ffmpeg: Path, source: Path, stream: int, work_dir: Path,
f"{ffmpeg}",
"-y",
"-i",
- f"{unixy(source)}",
+ f"{clean_file_string(source)}",
"-map",
f"0:{stream}",
"-c",
"copy",
"-vframes",
"1",
- f"{unixy(file_name)}",
+ f"{clean_file_string(file_name)}",
],
work_dir=work_dir,
timeout=5,
@@ -232,13 +217,10 @@ def extract_attachment(ffmpeg: Path, source: Path, stream: int, work_dir: Path,
def generate_thumbnail_command(
config: Config, source: Path, output: Path, filters: str, start_time: float = 0, input_track: int = 0
) -> str:
- start = ""
- if start_time:
- start = f"-ss {start_time}"
return (
- f'"{config.ffmpeg}" {start} -loglevel error -i "{unixy(source)}" '
+ f'"{config.ffmpeg}" -ss {start_time} -loglevel error -i "{clean_file_string(source)}" '
f" {filters} -an -y -map_metadata -1 -map 0:{input_track} "
- f'-vframes 1 "{unixy(output)}" '
+ f'-vframes 1 "{clean_file_string(output)}" '
)
@@ -260,7 +242,7 @@ def get_auto_crop(
"-ss",
f"{start_time}",
"-i",
- f"{unixy(source)}",
+ f"{clean_file_string(source)}",
"-map",
f"0:{input_track}",
"-vf",
@@ -303,25 +285,29 @@ def detect_interlaced(app: FastFlixApp, config: Config, source: Path, **_):
# [Parsed_idet_0 @ 00000] Single frame detection: TFF: 0 BFF: 0 Progressive: 641 Undetermined: 359
# [Parsed_idet_0 @ 00000] Multi frame detection: TFF: 0 BFF: 0 Progressive: 953 Undetermined: 47
- output = execute(
- [
- f"{config.ffmpeg}",
- "-hide_banner",
- "-i",
- f"{unixy(source)}",
- "-vf",
- "idet",
- "-frames:v",
- "100",
- "-an",
- "-sn",
- "-dn",
- "-f",
- "rawvideo",
- f"{'NUL' if reusables.win_based else '/dev/null'}",
- "-y",
- ]
- )
+ try:
+ output = execute(
+ [
+ f"{config.ffmpeg}",
+ "-hide_banner",
+ "-i",
+ f"{clean_file_string(source)}",
+ "-vf",
+ "idet",
+ "-frames:v",
+ "100",
+ "-an",
+ "-sn",
+ "-dn",
+ "-f",
+ "rawvideo",
+ f"{'NUL' if reusables.win_based else '/dev/null'}",
+ "-y",
+ ]
+ )
+ except Exception:
+ logger.exception("Error while running the interlace detection command")
+ return
for line in output.stderr.splitlines():
if "Single frame detection" in line:
@@ -334,7 +320,7 @@ def detect_interlaced(app: FastFlixApp, config: Config, source: Path, **_):
else:
if int(tffs) + int(bffs) > int(progressive):
app.fastflix.current_video.video_settings.deinterlace = True
- app.fastflix.current_video.interlaced = True
+ app.fastflix.current_video.interlaced = "tff" if int(tffs) > int(bffs) else "bff"
return
app.fastflix.current_video.video_settings.deinterlace = False
app.fastflix.current_video.interlaced = False
@@ -397,49 +383,103 @@ def parse_hdr_details(app: FastFlixApp, **_):
logger.exception(f"Unexpected error while processing master-display from {streams.video[0]}")
else:
if master_display:
- app.fastflix.current_video.master_display = master_display
- app.fastflix.current_video.cll = cll
- return
+ app.fastflix.current_video.hdr10_streams.append(
+ Box(index=video_stream.index, master_display=master_display, cll=cll)
+ )
+ continue
+
+ result = execute(
+ [
+ f"{app.fastflix.config.ffprobe}",
+ "-loglevel",
+ "panic",
+ "-select_streams",
+ f"v:{video_stream.index}",
+ "-print_format",
+ "json",
+ "-show_frames",
+ "-read_intervals",
+ "%+#1",
+ "-show_entries",
+ "frame=color_space,color_primaries,color_transfer,side_data_list,pix_fmt",
+ f"{clean_file_string(app.fastflix.current_video.source)}",
+ ]
+ )
- result = execute(
- [
- f"{app.fastflix.config.ffprobe}",
- "-loglevel",
- "panic",
- "-select_streams",
- f"v:{video_track}",
- "-print_format",
- "json",
- "-show_frames",
- "-read_intervals",
- "%+#1",
- "-show_entries",
- "frame=color_space,color_primaries,color_transfer,side_data_list,pix_fmt",
- f"{unixy(app.fastflix.current_video.source)}",
- ]
- )
+ try:
+ data = Box.from_json(result.stdout, default_box=True, default_box_attr="")
+ except BoxError:
+ # Could not parse details
+ logger.error(
+ "COULD NOT PARSE FFPROBE HDR METADATA, PLEASE OPEN ISSUE WITH THESE DETAILS:"
+ f"\nSTDOUT: {result.stdout}\nSTDERR: {result.stderr}"
+ )
+ continue
+ if "frames" not in data or not len(data.frames):
+ continue
+ data = data.frames[0]
+ if not data.get("side_data_list"):
+ continue
- try:
- data = Box.from_json(result.stdout, default_box=True, default_box_attr="")
- except BoxError:
- # Could not parse details
- logger.error(
- "COULD NOT PARSE FFPROBE HDR METADATA, PLEASE OPEN ISSUE WITH THESE DETAILS:"
- f"\nSTDOUT: {result.stdout}\nSTDERR: {result.stderr}"
- )
- return
- if "frames" not in data or not len(data.frames):
- return
- data = data.frames[0]
- if not data.get("side_data_list"):
+ try:
+ master_display, cll = convert_mastering_display(data)
+ except FlixError as err:
+ logger.error(str(err))
+ except Exception:
+ logger.exception(f"Unexpected error while processing master-display from {streams.video[0]}")
+ else:
+ if master_display:
+ app.fastflix.current_video.hdr10_streams.append(
+ Box(index=video_stream.index, master_display=master_display, cll=cll)
+ )
+
+
+def detect_hdr10_plus(app: FastFlixApp, config: Config, **_):
+ if not config.hdr10plus_parser or not config.hdr10plus_parser.exists():
return
- try:
- master_display, cll = convert_mastering_display(data)
- except FlixError as err:
- logger.error(str(err))
- except Exception:
- logger.exception(f"Unexpected error while processing master-display from {streams.video[0]}")
- else:
- app.fastflix.current_video.master_display = master_display
- app.fastflix.current_video.cll = cll
+ hdr10plus_streams = []
+
+ for stream in app.fastflix.current_video.streams.video:
+ logger.debug(f"Checking for hdr10+ in stream {stream.index}")
+ process = Popen(
+ [
+ config.ffmpeg,
+ "-y",
+ "-i",
+ clean_file_string(app.fastflix.current_video.source),
+ "-map",
+ f"0:{stream.index}",
+ "-loglevel",
+ "panic",
+ "-c:v",
+ "copy",
+ "-vbsf",
+ "hevc_mp4toannexb",
+ "-f",
+ "hevc",
+ "-",
+ ],
+ stdout=PIPE,
+ stderr=PIPE,
+ stdin=PIPE, # FFmpeg can try to read stdin and wrecks havoc
+ )
+
+ process_two = Popen(
+ [config.hdr10plus_parser, "--verify", "-"],
+ stdout=PIPE,
+ stderr=PIPE,
+ stdin=process.stdout,
+ encoding="utf-8",
+ )
+
+ try:
+ stdout, stderr = process_two.communicate()
+ except Exception:
+ logger.exception(f"Unexpected error while trying to detect HDR10+ metadata in stream {stream.index}")
+ else:
+ if "Dynamic HDR10+ metadata detected." in stdout:
+ hdr10plus_streams.append(stream.index)
+
+ if hdr10plus_streams:
+ app.fastflix.current_video.hdr10_plus = hdr10plus_streams
diff --git a/fastflix/models/base.py b/fastflix/models/base.py
deleted file mode 100644
index 710b56da..00000000
--- a/fastflix/models/base.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# -*- coding: utf-8 -*-
-import logging
-from multiprocessing import Queue
-
-logger = logging.getLogger("fastflix")
-
-ignore_list = [Queue]
-
-NO_OPTION = object()
-
-
-class BaseDataClass:
- def __setattr__(self, key, value):
- if value is not None and key in self.__class__.__annotations__:
- annotation = self.__class__.__annotations__[key]
- if hasattr(annotation, "__args__") and getattr(annotation, "_name", "") == "Union":
- annotation = annotation.__args__
- elif hasattr(annotation, "_name"):
- # Assuming this is a typing object we can't handle
- return super().__setattr__(key, value)
- if annotation in ignore_list:
- return super().__setattr__(key, value)
- try:
- if not isinstance(value, annotation):
- raise ValueError(
- f'"{key}" attempted to be set to "{value}" of type "{type(value)}" but must be of type "{annotation}"'
- )
- except TypeError as err:
- logger.debug(f"Could not validate type for {key} with {annotation}: {err}")
- return super().__setattr__(key, value)
-
- # def get(self, item, default=NO_OPTION):
- # if default != NO_OPTION:
- # return getattr(self, item, default)
- # return getattr(self, item)
diff --git a/fastflix/models/config.py b/fastflix/models/config.py
index 88e4ca3f..6c776837 100644
--- a/fastflix/models/config.py
+++ b/fastflix/models/config.py
@@ -1,3 +1,4 @@
+#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import shutil
@@ -14,12 +15,16 @@
AOMAV1Settings,
CopySettings,
GIFSettings,
+ FFmpegNVENCSettings,
SVTAV1Settings,
VP9Settings,
WebPSettings,
rav1eSettings,
x264Settings,
x265Settings,
+ NVEncCSettings,
+ NVEncCAVCSettings,
+ setting_types,
)
from fastflix.version import __version__
@@ -30,17 +35,6 @@
NO_OPT = object()
-setting_types = {
- "x265": x265Settings,
- "x264": x264Settings,
- "rav1e": rav1eSettings,
- "svt_av1": SVTAV1Settings,
- "vp9": VP9Settings,
- "aom_av1": AOMAV1Settings,
- "gif": GIFSettings,
- "webp": WebPSettings,
- "copy_settings": CopySettings,
-}
outdated_settings = ("copy",)
@@ -77,6 +71,9 @@ class Profile(BaseModel):
gif: Optional[GIFSettings] = None
webp: Optional[WebPSettings] = None
copy_settings: Optional[CopySettings] = None
+ ffmpeg_hevc_nvenc: Optional[FFmpegNVENCSettings] = None
+ nvencc_hevc: Optional[NVEncCSettings] = None
+ nvencc_avc: Optional[NVEncCAVCSettings] = None
empty_profile = Profile(x265=x265Settings())
@@ -88,7 +85,7 @@ def get_preset_defaults():
"UHD HDR10 Film": Profile(
auto_crop=True, x265=x265Settings(crf=18, hdr10=True, hdr10_opt=True, repeat_headers=True, preset="slow")
),
- "1080p Film": Profile(auto_crop=True, encoder="AVC (x264)", x264=x264Settings(crf=17, preset="slow")),
+ "1080p Film": Profile(auto_crop=True, encoder="AVC (x264)", x264=x264Settings(crf=22, preset="slow")),
}
@@ -113,14 +110,25 @@ def find_ffmpeg_file(name, raise_on_missing=False):
return None
+def where(filename: str) -> Optional[Path]:
+ if location := shutil.which(filename):
+ return Path(location)
+ return None
+
+
class Config(BaseModel):
version: str = __version__
config_path: Path = fastflix_folder / "fastflix.yaml"
ffmpeg: Path = Field(default_factory=lambda: find_ffmpeg_file("ffmpeg"))
ffprobe: Path = Field(default_factory=lambda: find_ffmpeg_file("ffprobe"))
+ hdr10plus_parser: Optional[Path] = Field(default_factory=lambda: where("hdr10plus_parser"))
+ mkvpropedit: Optional[Path] = Field(default_factory=lambda: where("mkvpropedit"))
+ nvencc: Optional[Path] = Field(default_factory=lambda: where("NVEncC"))
+ output_directory: Optional[Path] = False
flat_ui: bool = True
language: str = "en"
logging_level: int = 10
+ crop_detect_points: int = 10
continue_on_failure: bool = True
work_path: Path = fastflix_folder
use_sane_audio: bool = True
@@ -186,7 +194,7 @@ def load(self):
"there may be non-recoverable errors while loading it."
)
- paths = ("work_path", "ffmpeg", "ffprobe")
+ paths = ("work_path", "ffmpeg", "ffprobe", "hdr10plus_parser", "mkvpropedit", "nvencc", "output_directory")
for key, value in data.items():
if key == "profiles":
self.profiles = {}
@@ -223,6 +231,12 @@ def load(self):
self.ffprobe = find_ffmpeg_file("ffmpeg.ffprobe", raise_on_missing=True)
except MissingFF:
raise err from None
+ if not self.hdr10plus_parser:
+ self.hdr10plus_parser = where("hdr10plus_parser")
+ if not self.mkvpropedit:
+ self.mkvpropedit = where("mkvpropedit")
+ if not self.nvencc:
+ self.mkvpropedit = where("NVEncC")
self.profiles.update(get_preset_defaults())
if self.selected_profile not in self.profiles:
diff --git a/fastflix/models/encode.py b/fastflix/models/encode.py
index 9652fcc2..17d8fc95 100644
--- a/fastflix/models/encode.py
+++ b/fastflix/models/encode.py
@@ -1,19 +1,27 @@
+#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pathlib import Path
-from typing import List, Optional, Union
+from typing import List, Optional, Union, Dict
from pydantic import BaseModel, Field
+from box import Box
class AudioTrack(BaseModel):
index: int
outdex: int
codec: str = ""
- downmix: int = 0
+ downmix: Optional[str] = None
title: str = ""
language: str = ""
conversion_bitrate: str = ""
conversion_codec: str = ""
+ profile: Optional[str] = None
+ enabled: bool = True
+ original: bool = False
+ channels: int = 2
+ friendly_info: str = ""
+ raw_info: Optional[Union[Dict, Box]] = None
class SubtitleTrack(BaseModel):
@@ -73,6 +81,84 @@ class x264Settings(EncoderSettings):
bitrate: Optional[str] = None
+class FFmpegNVENCSettings(EncoderSettings):
+ name = "HEVC (NVENC)"
+ preset: str = "slow"
+ profile: str = "main"
+ tune: str = "hq"
+ pix_fmt: str = "p010le"
+ bitrate: Optional[str] = "6000k"
+ qp: Optional[str] = None
+ cq: int = 0
+ spatial_aq: int = 0
+ rc_lookahead: int = 0
+ rc: Optional[str] = None
+ tier: str = "main"
+ level: Optional[str] = None
+ gpu: int = -1
+ b_ref_mode: str = "disabled"
+
+
+class NVEncCSettings(EncoderSettings):
+ name = "HEVC (NVEncC)"
+ preset: str = "quality"
+ profile: str = "auto"
+ bitrate: Optional[str] = "5000k"
+ cqp: Optional[str] = None
+ aq: str = "off"
+ aq_strength: int = 0
+ lookahead: Optional[int] = None
+ tier: str = "high"
+ level: Optional[str] = None
+ hdr10plus_metadata: str = ""
+ multipass: str = "2pass-full"
+ mv_precision: str = "Auto"
+ init_q_i: Optional[str] = None
+ init_q_p: Optional[str] = None
+ init_q_b: Optional[str] = None
+ min_q_i: Optional[str] = None
+ min_q_p: Optional[str] = None
+ min_q_b: Optional[str] = None
+ max_q_i: Optional[str] = None
+ max_q_p: Optional[str] = None
+ max_q_b: Optional[str] = None
+ vbr_target: Optional[str] = None
+ b_frames: Optional[str] = None
+ b_ref_mode: str = "disabled"
+ ref: Optional[str] = None
+ metrics: bool = True
+
+
+class NVEncCAVCSettings(EncoderSettings):
+ name = "AVC (NVEncC)"
+ preset: str = "quality"
+ profile: str = "auto"
+ bitrate: Optional[str] = "5000k"
+ cqp: Optional[str] = None
+ aq: str = "off"
+ aq_strength: int = 0
+ lookahead: Optional[int] = None
+ tier: str = "high"
+ level: Optional[str] = None
+ hdr10plus_metadata: str = ""
+ multipass: str = "2pass-full"
+ mv_precision: str = "Auto"
+ init_q_i: Optional[str] = None
+ init_q_p: Optional[str] = None
+ init_q_b: Optional[str] = None
+ min_q_i: Optional[str] = None
+ min_q_p: Optional[str] = None
+ min_q_b: Optional[str] = None
+ max_q_i: Optional[str] = None
+ max_q_p: Optional[str] = None
+ max_q_b: Optional[str] = None
+ vbr_target: Optional[str] = None
+ b_frames: Optional[str] = None
+ b_ref_mode: str = "disabled"
+ ref: Optional[str] = None
+ metrics: bool = True
+
+
class rav1eSettings(EncoderSettings):
name = "AV1 (rav1e)"
speed: str = "-1"
@@ -105,6 +191,9 @@ class VP9Settings(EncoderSettings):
single_pass: bool = False
crf: Optional[Union[int, float]] = 31
bitrate: Optional[str] = None
+ fast_first_pass: Optional[bool] = True
+ tile_columns: str = "-1"
+ tile_rows: str = "-1"
class AOMAV1Settings(EncoderSettings):
@@ -134,3 +223,19 @@ class GIFSettings(EncoderSettings):
class CopySettings(EncoderSettings):
name = "Copy"
+
+
+setting_types = {
+ "x265": x265Settings,
+ "x264": x264Settings,
+ "rav1e": rav1eSettings,
+ "svt_av1": SVTAV1Settings,
+ "vp9": VP9Settings,
+ "aom_av1": AOMAV1Settings,
+ "gif": GIFSettings,
+ "webp": WebPSettings,
+ "copy_settings": CopySettings,
+ "ffmpeg_hevc_nvenc": FFmpegNVENCSettings,
+ "nvencc_hevc": NVEncCSettings,
+ "nvencc_avc": NVEncCAVCSettings,
+}
diff --git a/fastflix/models/fastflix.py b/fastflix/models/fastflix.py
index 6bb47c65..13d25196 100644
--- a/fastflix/models/fastflix.py
+++ b/fastflix/models/fastflix.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from dataclasses import dataclass, field
-# from multiprocessing import Queue
+from multiprocessing import Lock
from pathlib import Path
from typing import Any, Dict, List, Optional
@@ -18,6 +18,7 @@ class FastFlix(BaseModel):
config: Config = None
data_path: Path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True))
log_path: Path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "logs"
+ queue_path: Path = Path(user_data_dir("FastFlix", appauthor=False, roaming=True)) / "queue.yaml"
ffmpeg_version: str = ""
ffmpeg_config: List[str] = ""
ffprobe_version: str = ""
@@ -28,4 +29,5 @@ class FastFlix(BaseModel):
log_queue: Any = None
current_video: Optional[Video] = None
- queue: List[Video] = Field(default_factory=list)
+ queue: Any = None
+ queue_lock: Any = None
diff --git a/fastflix/models/video.py b/fastflix/models/video.py
index ef5fd043..f23c21da 100644
--- a/fastflix/models/video.py
+++ b/fastflix/models/video.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import uuid
from pathlib import Path
-from typing import List, Optional, Union
+from typing import List, Optional, Union, Tuple
from box import Box
from pydantic import BaseModel, Field
@@ -12,6 +12,7 @@
AudioTrack,
CopySettings,
GIFSettings,
+ FFmpegNVENCSettings,
SubtitleTrack,
SVTAV1Settings,
VP9Settings,
@@ -19,13 +20,45 @@
rav1eSettings,
x264Settings,
x265Settings,
+ NVEncCSettings,
+ NVEncCAVCSettings,
)
-__all__ = ["VideoSettings", "Status", "Video"]
+__all__ = ["VideoSettings", "Status", "Video", "Crop", "Status"]
+
+
+def determine_rotation(streams, track: int = 0) -> Tuple[int, int]:
+ for stream in streams.video:
+ if int(track) == stream["index"]:
+ video_stream = stream
+ break
+ else:
+ return 0, 0
+
+ rotation = 0
+ if "rotate" in streams.video[0].get("tags", {}):
+ rotation = abs(int(video_stream.tags.rotate))
+
+ if rotation in (90, 270):
+ video_width = video_stream.height
+ video_height = video_stream.width
+ else:
+ video_width = video_stream.width
+ video_height = video_stream.height
+ return video_width, video_height
+
+
+class Crop(BaseModel):
+ top: int = 0
+ right: int = 0
+ bottom: int = 0
+ left: int = 0
+ width: int = 0
+ height: int = 0
class VideoSettings(BaseModel):
- crop: Optional[str] = None
+ crop: Optional[Crop] = None
start_time: Union[float, int] = 0
end_time: Union[float, int] = 0
fast_seek: bool = True
@@ -63,6 +96,9 @@ class VideoSettings(BaseModel):
GIFSettings,
WebPSettings,
CopySettings,
+ FFmpegNVENCSettings,
+ NVEncCSettings,
+ NVEncCAVCSettings,
] = None
audio_tracks: List[AudioTrack] = Field(default_factory=list)
subtitle_tracks: List[SubtitleTrack] = Field(default_factory=list)
@@ -76,28 +112,63 @@ class Status(BaseModel):
complete: bool = False
running: bool = False
cancelled: bool = False
+ subtitle_fixed: bool = False
current_command: int = 0
+ @property
+ def ready(self) -> bool:
+ return not self.success and not self.error and not self.complete and not self.running and not self.cancelled
+
+ def clear(self):
+ self.success = False
+ self.error = False
+ self.complete = False
+ self.running = False
+ self.cancelled = False
+ self.subtitle_fixed = False
+ self.current_command = 0
+
class Video(BaseModel):
source: Path
- width: int = 0
- height: int = 0
duration: Union[float, int] = 0
streams: Box = None
work_path: Path = None
format: Box = None
- interlaced: bool = True
+ interlaced: Union[str, bool] = False
- # HDR10 Details
- master_display: Box = None
- cll: str = ""
+ hdr10_streams: List[Box] = Field(default_factory=list)
+ hdr10_plus: List[int] = Field(default_factory=list)
video_settings: VideoSettings = Field(default_factory=VideoSettings)
status: Status = Field(default_factory=Status)
uuid: str = Field(default_factory=lambda: str(uuid.uuid4()))
+ @property
+ def width(self):
+ w, _ = determine_rotation(self.streams, self.video_settings.selected_track)
+ return w
+
+ @property
+ def height(self):
+ _, h = determine_rotation(self.streams, self.video_settings.selected_track)
+ return h
+
+ @property
+ def master_display(self) -> Optional[Box]:
+ for track in self.hdr10_streams:
+ if track.index == self.video_settings.selected_track:
+ return track["master_display"]
+ return None
+
+ @property
+ def cll(self) -> Optional[str]:
+ for track in self.hdr10_streams:
+ if track.index == self.video_settings.selected_track:
+ return track["cll"]
+ return None
+
@property
def current_video_stream(self):
try:
diff --git a/fastflix/program_downloads.py b/fastflix/program_downloads.py
index e640273d..427b6d14 100644
--- a/fastflix/program_downloads.py
+++ b/fastflix/program_downloads.py
@@ -61,6 +61,7 @@ def stop_me():
raise
if stop:
+ message(t("Download Cancelled"))
return
gpl_ffmpeg = [asset for asset in data["assets"] if asset["name"].endswith("win64-gpl.zip")]
@@ -82,6 +83,9 @@ def stop_me():
signal.emit(int(((i * 1024) / gpl_ffmpeg[0]["size"]) * 90))
f.write(block)
if stop:
+ f.close()
+ Path(filename).unlink()
+ message(t("Download Cancelled"))
return
if filename.stat().st_size < 1000:
@@ -99,6 +103,8 @@ def stop_me():
raise
if stop:
+ Path(filename).unlink()
+ message(t("Download Cancelled"))
return
signal.emit(95)
diff --git a/fastflix/queue.py b/fastflix/queue.py
new file mode 100644
index 00000000..440c9209
--- /dev/null
+++ b/fastflix/queue.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+from typing import List
+import os
+from pathlib import Path
+import logging
+import shutil
+import uuid
+
+from box import Box, BoxError
+from ruamel.yaml import YAMLError
+
+from fastflix.models.video import Video, VideoSettings, Status, Crop
+from fastflix.models.encode import AudioTrack, SubtitleTrack, AttachmentTrack
+from fastflix.models.encode import setting_types
+from fastflix.models.config import Config
+
+logger = logging.getLogger("fastflix")
+
+
+def get_queue(queue_file: Path, config: Config) -> List[Video]:
+ if not queue_file.exists():
+ return []
+
+ try:
+ loaded = Box.from_yaml(filename=queue_file)
+ except (BoxError, YAMLError):
+ logger.exception("Could not open queue")
+ return []
+
+ queue = []
+ for video in loaded["queue"]:
+ video["source"] = Path(video["source"])
+ video["work_path"] = Path(video["work_path"])
+ video["video_settings"]["output_path"] = Path(video["video_settings"]["output_path"])
+ encoder_settings = video["video_settings"]["video_encoder_settings"]
+ ves = [x(**encoder_settings) for x in setting_types.values() if x().name == encoder_settings["name"]][0]
+ audio = [AudioTrack(**x) for x in video["video_settings"]["audio_tracks"]]
+ subtitles = [SubtitleTrack(**x) for x in video["video_settings"]["subtitle_tracks"]]
+ attachments = []
+ for x in video["video_settings"]["attachment_tracks"]:
+ try:
+ attachment_path = x.pop("file_path")
+ except KeyError:
+ attachment_path = None
+ attachment = AttachmentTrack(**x)
+ attachment.file_path = Path(attachment_path)
+ attachments.append(attachment)
+ status = Status(**video["status"])
+ crop = None
+ if video["video_settings"]["crop"]:
+ crop = Crop(**video["video_settings"]["crop"])
+ del video["video_settings"]["audio_tracks"]
+ del video["video_settings"]["subtitle_tracks"]
+ del video["video_settings"]["attachment_tracks"]
+ del video["video_settings"]["video_encoder_settings"]
+ del video["status"]
+ del video["video_settings"]["crop"]
+ vs = VideoSettings(
+ **video["video_settings"],
+ audio_tracks=audio,
+ subtitle_tracks=subtitles,
+ attachment_tracks=attachments,
+ crop=crop,
+ )
+ vs.video_encoder_settings = ves # No idea why this has to be called after, otherwise reset to x265
+ del video["video_settings"]
+ queue.append(Video(**video, video_settings=vs, status=status))
+ return queue
+
+
+def save_queue(queue: List[Video], queue_file: Path, config: Config):
+ items = []
+ queue_covers = config.work_path / "covers"
+ queue_covers.mkdir(parents=True, exist_ok=True)
+ queue_data = config.work_path / "queue_extras"
+ queue_data.mkdir(parents=True, exist_ok=True)
+
+ def update_conversion_command(vid, old_path: str, new_path: str):
+ for command in vid["video_settings"]["conversion_commands"]:
+ new_command = command["command"].replace(old_path, new_path)
+ if new_command == command["command"]:
+ logger.error(f'Could not replace "{old_path}" with "{new_path}" in {command["command"]}')
+ command["command"] = new_command
+
+ for video in queue:
+ video = video.dict()
+ video["source"] = os.fspath(video["source"])
+ video["work_path"] = os.fspath(video["work_path"])
+ video["video_settings"]["output_path"] = os.fspath(video["video_settings"]["output_path"])
+ if metadata := video["video_settings"]["video_encoder_settings"].get("hdr10plus_metadata"):
+ new_metadata_file = queue_data / f"{uuid.uuid4().hex}_metadata.json"
+ try:
+ shutil.copy(metadata, new_metadata_file)
+ except OSError:
+ logger.exception("Could not save HDR10+ metadata file to queue recovery location, removing HDR10+")
+
+ update_conversion_command(
+ video,
+ str(metadata),
+ str(new_metadata_file),
+ )
+ video["video_settings"]["video_encoder_settings"]["hdr10plus_metadata"] = str(new_metadata_file)
+ for track in video["video_settings"]["attachment_tracks"]:
+ if track.get("file_path"):
+ new_file = queue_covers / f'{uuid.uuid4().hex}_{track["file_path"].name}'
+ try:
+ shutil.copy(track["file_path"], new_file)
+ except OSError:
+ logger.exception("Could not save cover to queue recovery location, removing cover")
+ update_conversion_command(video, str(track["file_path"]), str(new_file))
+ track["file_path"] = str(new_file)
+
+ items.append(video)
+ Box(queue=items).to_yaml(filename=queue_file)
+ logger.debug(f"queue saved to recovery file {queue_file}")
diff --git a/fastflix/resources.py b/fastflix/resources.py
index d306eca0..b3b95c2b 100644
--- a/fastflix/resources.py
+++ b/fastflix/resources.py
@@ -36,4 +36,5 @@
working_icon = str(Path(pkg_resources.resource_filename(__name__, "data/icons/pending-work.png")).resolve())
advanced_icon = str(Path(pkg_resources.resource_filename(__name__, "data/icons/advanced.png")).resolve())
info_icon = str(Path(pkg_resources.resource_filename(__name__, "data/icons/info.png")).resolve())
+undo_icon = str(Path(pkg_resources.resource_filename(__name__, "data/icons/undo-arrow.png")).resolve())
loading_movie = str(Path(pkg_resources.resource_filename(__name__, "data/icons/loading.gif")).resolve())
diff --git a/fastflix/shared.py b/fastflix/shared.py
index f540af6d..0b4f9e5d 100644
--- a/fastflix/shared.py
+++ b/fastflix/shared.py
@@ -11,6 +11,8 @@
import pkg_resources
import requests
import reusables
+from pathvalidate import sanitize_filepath
+
try:
# PyInstaller creates a temp folder and stores path in _MEIPASS
@@ -25,8 +27,6 @@
from fastflix.language import t
-QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
-QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
DEVMODE = os.getenv("DEVMODE", "").lower() in ("1", "true")
my_data = str(Path(pkg_resources.resource_filename(__name__, f"../data/icon.ico")).resolve())
@@ -226,7 +226,7 @@ def clean_logs(signal, app, **_):
except UnicodeDecodeError:
pass
else:
- if len(condensed) < len(original):
+ if (len(condensed) + 100) < len(original):
logger.debug(f"Compressed {file.name} from {len(original)} characters to {len(condensed)}")
file.write_text(condensed, encoding="utf-8")
if is_old:
@@ -256,3 +256,12 @@ def timedelta_to_str(delta):
output_string = output_string.split(".")[0] # Remove .XXX microseconds
return output_string
+
+
+def clean_file_string(source):
+ return str(source).strip().strip("'\"")
+
+
+def sanitize(source):
+ return str(sanitize_filepath(source, platform="Windows" if reusables.win_based else "Linux"))
+ # return str().replace("\\", "/")
diff --git a/fastflix/version.py b/fastflix/version.py
index aab4fbdb..a323c5f2 100644
--- a/fastflix/version.py
+++ b/fastflix/version.py
@@ -1,4 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
-__version__ = "4.1.2"
+__version__ = "4.2.0"
__author__ = "Chris Griffith"
diff --git a/fastflix/widgets/about.py b/fastflix/widgets/about.py
index 7a35a138..48c2f6ad 100644
--- a/fastflix/widgets/about.py
+++ b/fastflix/widgets/about.py
@@ -18,7 +18,7 @@ def __init__(self, parent=None):
super(About, self).__init__(parent)
layout = QtWidgets.QGridLayout()
- self.setMinimumSize(400, 400)
+ self.setMinimumSize(QtCore.QSize(400, 400))
build_file = Path(base_path, "build_version")
@@ -33,28 +33,38 @@ def __init__(self, parent=None):
label.setAlignment(QtCore.Qt.AlignCenter)
label.setOpenExternalLinks(True)
label.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
+ layout.addWidget(label)
+
+ support_label = QtWidgets.QLabel(
+ f'{link("https://github.com/cdgriffith/FastFlix/wiki/Support-FastFlix", t("Support FastFlix"))}
'
+ )
+ support_label.setOpenExternalLinks(True)
+ support_label.setFont(QtGui.QFont("Arial", 12))
+ support_label.setAlignment((QtCore.Qt.AlignCenter | QtCore.Qt.AlignTop))
+ layout.addWidget(support_label)
+
+ bundle_label = QtWidgets.QLabel(
+ f"Conversion suites: {link('https://www.ffmpeg.org/download.html', 'FFmpeg')} ({t('Various')}), "
+ f"{link('https://github.com/rigaya/NVEnc', 'NVEncC')} (MIT)
"
+ f"Encoders:
{link('https://github.com/rigaya/NVEnc', 'NVEncC')} (MIT), "
+ f"SVT AV1 (MIT), rav1e (MIT), aom (MIT), x265 (GPL), x264 (GPL), libvpx (BSD)"
+ )
+ bundle_label.setAlignment(QtCore.Qt.AlignCenter)
+ bundle_label.setOpenExternalLinks(True)
+ layout.addWidget(bundle_label)
supporting_libraries_label = QtWidgets.QLabel(
"Supporting libraries
"
f"{link('https://www.python.org/', t('Python'))}{reusables.version_string} (PSF LICENSE), "
f"{link('https://github.com/cdgriffith/Box', t('python-box'))} {box_version} (MIT), "
f"{link('https://github.com/cdgriffith/Reusables', t('Reusables'))} {reusables.__version__} (MIT)
"
- "mistune (BSD), colorama (BSD), coloredlogs (MIT), Requests (Apache 2.0)"
+ "mistune (BSD), colorama (BSD), coloredlogs (MIT), Requests (Apache 2.0)
"
+ "appdirs (MIT), iso639-lang (MIT), psutil (BSD), qtpy (MIT), pathvalidate (MIT)
"
)
supporting_libraries_label.setAlignment(QtCore.Qt.AlignCenter)
supporting_libraries_label.setOpenExternalLinks(True)
-
- layout.addWidget(label)
layout.addWidget(supporting_libraries_label)
- bundle_label = QtWidgets.QLabel(
- f"Conversion suite: {link('https://www.ffmpeg.org/download.html', 'FFmpeg')} ({t('Various')})
"
- "Encoders:
SVT AV1 (MIT), rav1e (MIT), aom (MIT), x265 (GPL), x264 (GPL), libvpx (BSD)"
- )
- bundle_label.setAlignment(QtCore.Qt.AlignCenter)
- bundle_label.setOpenExternalLinks(True)
- layout.addWidget(bundle_label)
-
if pyinstaller:
pyinstaller_label = QtWidgets.QLabel(
f"Packaged with: {link('https://www.pyinstaller.org/index.html', 'PyInstaller')}"
diff --git a/fastflix/widgets/background_tasks.py b/fastflix/widgets/background_tasks.py
index 5c31fedb..85b71a4e 100644
--- a/fastflix/widgets/background_tasks.py
+++ b/fastflix/widgets/background_tasks.py
@@ -8,10 +8,11 @@
from fastflix.language import t
from fastflix.models.fastflix_app import FastFlixApp
+from fastflix.shared import clean_file_string
logger = logging.getLogger("fastflix")
-__all__ = ["ThumbnailCreator", "ExtractSubtitleSRT", "SubtitleFix"]
+__all__ = ["ThumbnailCreator", "ExtractSubtitleSRT", "SubtitleFix", "ExtractHDR10"]
class ThumbnailCreator(QtCore.QThread):
@@ -47,7 +48,7 @@ def __init__(self, main, mkv_prop_edit, video_path):
self.video_path = video_path
def run(self):
- output_file = str(self.video_path).replace("\\", "/")
+ output_file = clean_file_string(self.video_path)
self.main.thread_logging_signal.emit(f'INFO:{t("Will fix first subtitle track to not be default")}')
try:
result = run(
@@ -108,49 +109,70 @@ def run(self):
self.signal.emit()
-# class ExtractHDR10(QtCore.QThread):
-# def __init__(self, app: FastFlixApp, main, index, signal):
-# super().__init__(main)
-# self.main = main
-# self.app = app
-# self.index = index
-# self.signal = signal
-#
-# def run(self):
-# # VERIFY ffmpeg -loglevel panic -i input.mkv -c:v copy -vbsf hevc_mp4toannexb -f hevc - | hdr10plus_parser --verify -
-#
-# self.main.thread_logging_signal.emit(f'INFO:{t("Extracting HDR10+ metadata")}')
-#
-# process = Popen(
-# [
-# self.app.fastflix.config.ffmpeg,
-# "-y",
-# "-i",
-# self.main.input_video,
-# "-map",
-# f"0:{self.index}",
-# "-loglevel",
-# "panic",
-# "-c:v",
-# "copy",
-# "-vbsf",
-# "hevc_mp4toannexb",
-# "-f",
-# "hevc",
-# "-"
-# ],
-# stdout=PIPE,
-# stderr=PIPE,
-# stdin=PIPE, # FFmpeg can try to read stdin and wrecks havoc
-# )
-#
-# process_two = Popen(
-# ["hdr10plus_parser", "--verify", "-"],
-# stdout=PIPE,
-# stderr=PIPE,
-# stdin=self.process.stdout,
-# encoding="utf-8",
-# )
-#
-# stdout, stderr = process_two.communicate()
-#
+class ExtractHDR10(QtCore.QThread):
+ def __init__(self, app: FastFlixApp, main, signal, ffmpeg_signal):
+ super().__init__(main)
+ self.main = main
+ self.app = app
+ self.signal = signal
+ self.ffmpeg_signal = ffmpeg_signal
+
+ def run(self):
+ if not self.app.fastflix.current_video.hdr10_plus:
+ self.main.thread_logging_signal.emit("ERROR:No tracks have HDR10+ data to extract")
+ return
+
+ output = self.app.fastflix.current_video.work_path / "metadata.json"
+
+ track = self.app.fastflix.current_video.video_settings.selected_track
+ if track not in self.app.fastflix.current_video.hdr10_plus:
+ self.main.thread_logging_signal.emit(
+ "WARNING:Selected video track not detected to have HDR10+ data, selecting first track that does"
+ )
+ track = self.app.fastflix.current_video.hdr10_plus[0]
+
+ self.main.thread_logging_signal.emit(f'INFO:{t("Extracting HDR10+ metadata")} to {output}')
+
+ self.ffmpeg_signal.emit("Extracting HDR10+ metadata")
+
+ process = Popen(
+ [
+ self.app.fastflix.config.ffmpeg,
+ "-y",
+ "-i",
+ clean_file_string(self.app.fastflix.current_video.source),
+ "-map",
+ f"0:{track}",
+ "-c:v",
+ "copy",
+ "-vbsf",
+ "hevc_mp4toannexb",
+ "-f",
+ "hevc",
+ "-",
+ ],
+ stdout=PIPE,
+ stderr=open(self.app.fastflix.current_video.work_path / "hdr10extract_out.txt", "wb"),
+ # stdin=PIPE, # FFmpeg can try to read stdin and wrecks havoc
+ )
+
+ process_two = Popen(
+ [self.app.fastflix.config.hdr10plus_parser, "-o", clean_file_string(output), "-"],
+ stdout=PIPE,
+ stderr=PIPE,
+ stdin=process.stdout,
+ encoding="utf-8",
+ cwd=str(self.app.fastflix.current_video.work_path),
+ )
+
+ with open(self.app.fastflix.current_video.work_path / "hdr10extract_out.txt", "r", encoding="utf-8") as f:
+ while True:
+ if process.poll() is not None or process_two.poll() is not None:
+ break
+ if line := f.readline().rstrip():
+ if line.startswith("frame"):
+ self.ffmpeg_signal.emit(line)
+
+ stdout, stderr = process_two.communicate()
+ self.main.thread_logging_signal.emit(f"DEBUG: HDR10+ Extract: {stdout}")
+ self.signal.emit(str(output))
diff --git a/fastflix/widgets/container.py b/fastflix/widgets/container.py
index 727df493..2a1defed 100644
--- a/fastflix/widgets/container.py
+++ b/fastflix/widgets/container.py
@@ -18,6 +18,7 @@
from fastflix.program_downloads import latest_ffmpeg
from fastflix.resources import main_icon
from fastflix.shared import clean_logs, error_message, latest_fastflix, message
+from fastflix.windows_tools import cleanup_windows_notification
from fastflix.widgets.about import About
from fastflix.widgets.changes import Changes
from fastflix.widgets.logs import Logs
@@ -33,6 +34,7 @@ class Container(QtWidgets.QMainWindow):
def __init__(self, app: FastFlixApp, **kwargs):
super().__init__(None)
self.app = app
+ self.pb = None
self.logs = Logs()
self.changes = Changes()
@@ -45,11 +47,17 @@ def __init__(self, app: FastFlixApp, **kwargs):
self.profile = ProfileWindow(self.app, self.main)
self.setCentralWidget(self.main)
- self.setMinimumSize(QtCore.QSize(1200, 650))
+ self.setMinimumSize(QtCore.QSize(1280, 700))
self.icon = QtGui.QIcon(main_icon)
self.setWindowIcon(self.icon)
+ self.main.set_profile()
def closeEvent(self, a0: QtGui.QCloseEvent) -> None:
+ if self.pb:
+ try:
+ self.pb.stop_signal.emit()
+ except Exception:
+ pass
if self.main.converting:
sm = QtWidgets.QMessageBox()
sm.setText(f"